1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2003 Evan Martin <evan@livejournal.com>
9 #include "journalstore.h"
20 GtkWidget
*cal
, *searchentry
, *searchcase
;
21 GtkWidget
*summary
, *listbox
, *list
, *preview
;
22 GdkPixbuf
*pb_friends
, *pb_private
;
25 JournalStore
*journalstore
;
28 int current_itemid
; /* itemid of entry being previewed. useful to pop back the calendar after some browsing */
40 static GType
offlineui_get_type (void);
41 #define OFFLINEUI(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), offlineui_get_type(), OfflineUI))
44 //static const gint LJ_OFFLINE_JUMP_TO = 1;
45 #define LJ_OFFLINE_JUMP_TO (1)
46 #define LJ_OFFLINE_COPY_URL (2)
49 static void update_marks (OfflineUI
*oui
) {
53 gtk_calendar_get_date(GTK_CALENDAR(oui
->cal
), &year
, &mon
, NULL
);
54 days
= journal_store_get_month_entries(oui
->journalstore
, year
, mon
+1);
55 gtk_calendar_clear_marks(GTK_CALENDAR(oui
->cal
));
56 for (day
= 1; day
<= 31; ++day
) if (days
&(1<<day
)) gtk_calendar_mark_day(GTK_CALENDAR(oui
->cal
), day
);
60 static void month_changed_cb (GtkCalendar
*calendar
, OfflineUI
*oui
) {
65 static void get_entries_cb (int itemid
, time_t etime
, const char *summary
, LJSecurity
*sec
, gpointer data
) {
67 OfflineUI
*oui
= (OfflineUI
*)data
;
70 case LJ_SECURITY_FRIENDS
:
71 case LJ_SECURITY_CUSTOM
:
74 case LJ_SECURITY_PRIVATE
:
78 pb
= NULL
; /* public: no icon */
80 gtk_list_store_append(oui
->store
, &iter
);
81 gtk_list_store_set(oui
->store
, &iter
, COL_ITEMID
, itemid
, COL_DATE
, etime
, COL_SECURITY
, pb
, COL_SUMMARY
, summary
, -1);
85 static void load_day (OfflineUI
*oui
) {
87 gtk_calendar_get_date(GTK_CALENDAR(oui
->cal
), &year
, &mon
, &day
);
89 gtk_widget_destroy(oui
->summary
);
92 gtk_list_store_clear(oui
->store
);
93 journal_store_get_day_entries(oui
->journalstore
, year
, mon
+ 1, day
, get_entries_cb
, oui
);
97 static void day_selected_cb (GtkCalendar
*calendar
, OfflineUI
*oui
) {
100 if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(oui
->store
), &iter
)) {
101 gtk_tree_selection_select_iter(gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
)), &iter
);
106 static void selection_changed_cb (GtkTreeSelection
*ts
, OfflineUI
*oui
) {
109 gboolean sensitive
= gtk_tree_selection_get_selected(ts
, &model
, &iter
);
110 gtk_dialog_set_response_sensitive(GTK_DIALOG(oui
), LJ_OFFLINE_JUMP_TO
, sensitive
);
111 gtk_dialog_set_response_sensitive(GTK_DIALOG(oui
), LJ_OFFLINE_COPY_URL
, sensitive
);
112 gtk_dialog_set_response_sensitive(GTK_DIALOG(oui
), GTK_RESPONSE_OK
, sensitive
);
116 static void make_list (OfflineUI
*oui
) {
117 GtkCellRenderer
*renderer
;
118 GtkTreeViewColumn
*column
;
119 GtkTreeSelection
*sel
;
121 oui
->store
= gtk_list_store_new(4, G_TYPE_INT
, G_TYPE_LONG
, GDK_TYPE_PIXBUF
, G_TYPE_STRING
);
122 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(oui
->store
), COL_DATE
, GTK_SORT_ASCENDING
);
123 oui
->list
= gtk_tree_view_new_with_model(GTK_TREE_MODEL(oui
->store
));
124 g_object_unref(G_OBJECT(oui
->store
));
126 sel
= gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
));
127 g_signal_connect(G_OBJECT(sel
), "changed", G_CALLBACK(selection_changed_cb
), oui
);
129 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(oui
->list
), FALSE
);
131 column
= gtk_tree_view_column_new();
132 gtk_tree_view_column_set_title(column
, _("Summary"));
133 gtk_tree_view_column_set_sizing(column
, GTK_TREE_VIEW_COLUMN_AUTOSIZE
);
135 renderer
= gtk_cell_renderer_pixbuf_new();
136 gtk_tree_view_column_pack_start(column
, renderer
, FALSE
);
137 gtk_tree_view_column_add_attribute(column
, renderer
, "pixbuf", COL_SECURITY
);
139 renderer
= gtk_cell_renderer_text_new();
140 gtk_tree_view_column_pack_start(column
, renderer
, TRUE
);
141 gtk_tree_view_column_add_attribute(column
, renderer
, "text", COL_SUMMARY
);
143 gtk_tree_view_column_set_sort_column_id(column
, COL_SUMMARY
);
144 gtk_tree_view_append_column(GTK_TREE_VIEW(oui
->list
), column
);
148 static int get_selected_itemid (OfflineUI
*oui
) {
152 if (!gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
)), &model
, &iter
)) return 0;
153 gtk_tree_model_get(model
, &iter
, COL_ITEMID
, &itemid
, -1);
158 static LJEntry
*load_selected (OfflineUI
*oui
) {
160 int itemid
= get_selected_itemid(oui
);
161 if (!itemid
) return NULL
;
162 entry
= journal_store_get_entry(oui
->journalstore
, itemid
);
163 if (!entry
) g_warning("unable to find entry %d\n", itemid
);
168 static void select_entry (OfflineUI
*oui
, int itemid
) {
171 guint year
, mon
, day
;
173 GtkTreeModel
*model
= GTK_TREE_MODEL(oui
->store
);
174 GtkTreeSelection
*sel
= gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
));
177 etime
= journal_store_lookup_entry_time(oui
->journalstore
, itemid
);
179 etm
= gmtime(&etime
);
180 gtk_calendar_get_date(GTK_CALENDAR(oui
->cal
), &year
, &mon
, &day
);
182 gtk_calendar_freeze(GTK_CALENDAR(oui
->cal
));
183 g_signal_handlers_block_matched(oui
->cal
, G_SIGNAL_MATCH_DATA
, 0, 0, NULL
, NULL
, oui
);
185 /* the calendar gets confused in situations like switching into
186 * a 30-day month and have day 31 selected.
187 * so we move to the middle, switch months, and then move to the right place. */
188 if (day
!= etm
->tm_mday
) gtk_calendar_select_day(GTK_CALENDAR(oui
->cal
), 15);
189 if (year
!= etm
->tm_year
+1900 || mon
!= etm
->tm_mon
) gtk_calendar_select_month(GTK_CALENDAR(oui
->cal
), etm
->tm_mon
, etm
->tm_year
+1900);
190 if (day
!= etm
->tm_mday
) gtk_calendar_select_day(GTK_CALENDAR(oui
->cal
), etm
->tm_mday
);
191 if (day
!= etm
->tm_mday
) load_day(oui
);
192 if (year
!= etm
->tm_year
+1900 || mon
!= etm
->tm_mon
) update_marks(oui
);
194 g_signal_handlers_unblock_matched(oui
->cal
, G_SIGNAL_MATCH_DATA
, 0, 0, NULL
, NULL
, oui
);
195 gtk_calendar_thaw(GTK_CALENDAR(oui
->cal
));
197 if (gtk_tree_model_get_iter_first(model
, &iter
)) {
199 gtk_tree_model_get(model
, &iter
, COL_ITEMID
, &fitemid
, -1);
200 if (fitemid
== itemid
) {
201 gtk_tree_selection_select_iter(sel
, &iter
);
204 } while (gtk_tree_model_iter_next(model
, &iter
));
209 /* move the selection to the nearest next or previous item. The present
210 * selection need not be an actual entry. */
211 static void move_relative (OfflineUI
*oui
, int dir
) {
212 gboolean found
= FALSE
;
213 int itemid
= get_selected_itemid(oui
);
215 /* selection exists */
216 found
= journal_store_find_relative(oui
->journalstore
, itemid
, &itemid
, dir
, NULL
);
218 /* some more cajoling necessary */
219 guint year
, month
, day
;
220 struct tm when_tm
= { 0 };
221 gtk_calendar_get_date(GTK_CALENDAR(oui
->cal
), &year
, &month
, &day
);
222 when_tm
.tm_year
= year
-1900;
223 when_tm
.tm_mon
= month
;
224 when_tm
.tm_mday
= day
;
225 found
= journal_store_find_relative_by_time(oui
->journalstore
, lj_timegm(&when_tm
), &itemid
, dir
, NULL
);
227 if (found
) select_entry(oui
, itemid
);
231 static void prev_cb (GtkWidget
*button
, OfflineUI
*oui
) {
232 move_relative(oui
, -1);
236 static void next_cb (GtkWidget
*button
, OfflineUI
*oui
) {
237 move_relative(oui
, 1);
241 static GtkWidget
*make_cal (OfflineUI
*oui
) {
242 GtkWidget
*calbox
, *bbox
, *next
, *prev
;
244 calbox
= gtk_vbox_new(FALSE
, 6);
245 gtk_container_set_border_width(GTK_CONTAINER(calbox
), 6);
247 oui
->cal
= gtk_calendar_new();
248 gtk_calendar_display_options(GTK_CALENDAR(oui
->cal
), GTK_CALENDAR_SHOW_HEADING
);
250 g_signal_connect(G_OBJECT(oui
->cal
), "month_changed", G_CALLBACK(month_changed_cb
), oui
);
251 g_signal_connect(G_OBJECT(oui
->cal
), "day_selected", G_CALLBACK(day_selected_cb
), oui
);
252 gtk_box_pack_start(GTK_BOX(calbox
), oui
->cal
, FALSE
, FALSE
, 0);
254 bbox
= gtk_hbutton_box_new();
255 gtk_button_box_set_layout(GTK_BUTTON_BOX(bbox
), GTK_BUTTONBOX_EDGE
);
256 gtk_box_set_spacing(GTK_BOX(bbox
), 6);
257 prev
= gtk_button_new_from_stock(GTK_STOCK_GO_BACK
);
258 g_signal_connect(G_OBJECT(prev
), "clicked", G_CALLBACK(prev_cb
), oui
);
259 gtk_box_pack_start(GTK_BOX(bbox
), prev
, FALSE
, FALSE
, 0);
260 next
= gtk_button_new_from_stock(GTK_STOCK_GO_FORWARD
);
261 g_signal_connect(G_OBJECT(next
), "clicked", G_CALLBACK(next_cb
), oui
);
262 gtk_box_pack_start(GTK_BOX(bbox
), next
, FALSE
, FALSE
, 0);
264 gtk_box_pack_start(GTK_BOX(calbox
), bbox
, FALSE
, FALSE
, 0);
270 static gboolean
search_case_cb (const char *str
, gpointer data
) {
273 if (!str
) return FALSE
;
274 down
= g_utf8_strdown(str
, -1);
275 ret
= (strstr(down
, data
) != NULL
);
281 static gboolean
search_cb (const char *str
, gpointer data
) {
282 return (str
&& strstr(str
, data
) != NULL
);
286 static void find_cb (GtkWidget
*button
, OfflineUI
*oui
) {
290 search
= gtk_entry_get_text(GTK_ENTRY(oui
->searchentry
));
292 gtk_list_store_clear(oui
->store
);
293 if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(oui
->searchcase
))) {
294 journal_store_scan(oui
->journalstore
, search_cb
, (gpointer
) search
, get_entries_cb
, oui
);
296 char *lsearch
= g_utf8_strdown(search
, -1);
297 journal_store_scan(oui
->journalstore
, search_case_cb
, lsearch
, get_entries_cb
, oui
);
301 summary
= g_strdup_printf(_("Search found %d entries:"), gtk_tree_model_iter_n_children(GTK_TREE_MODEL(oui
->store
), NULL
));
303 oui
->summary
= gtk_label_new(summary
);
304 gtk_widget_show(oui
->summary
);
305 gtk_box_pack_start(GTK_BOX(oui
->listbox
), oui
->summary
, FALSE
, FALSE
, 0);
307 gtk_label_set_text(GTK_LABEL(oui
->summary
), summary
);
313 static GtkWidget
*make_search (OfflineUI
*oui
) {
314 GtkWidget
*box
, *l
, *bbox
, *button
;
316 box
= gtk_vbox_new(FALSE
, 6);
317 gtk_container_set_border_width(GTK_CONTAINER(box
), 6);
319 l
= gtk_label_new_with_mnemonic(_("_Search:"));
320 gtk_misc_set_alignment(GTK_MISC(l
), 0, 0.5);
321 gtk_box_pack_start(GTK_BOX(box
), l
, FALSE
, FALSE
, 0);
322 oui
->searchentry
= gtk_entry_new();
323 gtk_box_pack_start(GTK_BOX(box
), oui
->searchentry
, FALSE
, FALSE
, 0);
324 gtk_label_set_mnemonic_widget(GTK_LABEL(l
), oui
->searchentry
);
326 oui
->searchcase
= gtk_check_button_new_with_mnemonic(_("_Case sensitive"));
327 gtk_box_pack_start(GTK_BOX(box
), oui
->searchcase
, FALSE
, FALSE
, 0);
329 bbox
= gtk_hbutton_box_new();
330 gtk_button_box_set_layout(GTK_BUTTON_BOX(bbox
), GTK_BUTTONBOX_END
);
331 button
= gtk_button_new_from_stock(GTK_STOCK_FIND
);
332 g_signal_connect(G_OBJECT(button
), "clicked", G_CALLBACK(find_cb
), oui
);
333 g_signal_connect(G_OBJECT(oui
->searchentry
), "activate", G_CALLBACK(find_cb
), oui
);
334 gtk_box_pack_start(GTK_BOX(bbox
), button
, FALSE
, FALSE
, 0);
335 gtk_box_pack_start(GTK_BOX(box
), bbox
, FALSE
, FALSE
, 0);
341 static gboolean
check_entries (GtkWindow
*parent
, JamAccount
*acc
) {
345 dlg
= gtk_message_dialog_new(parent
, GTK_DIALOG_MODAL
,
346 GTK_MESSAGE_INFO
, GTK_BUTTONS_NONE
,
347 _("LogJam has no offline entries for this journal. "
348 "You must first synchronize this journal to your local " "storage before you can load entries from it."));
349 gtk_dialog_add_buttons(GTK_DIALOG(dlg
), _("_Synchronize"), GTK_RESPONSE_OK
, GTK_STOCK_CANCEL
, GTK_RESPONSE_CANCEL
, NULL
);
350 res
= gtk_dialog_run(GTK_DIALOG(dlg
));
351 gtk_widget_destroy(dlg
);
352 if (res
== GTK_RESPONSE_OK
) return sync_run(JAM_ACCOUNT_LJ(acc
), parent
);
358 static void content_update (OfflineUI
*oui
) {
360 GtkTextBuffer
*buffer
;
361 GtkTextIter start
, end
;
363 entry
= load_selected(oui
);
365 buffer
= gtk_text_view_get_buffer(GTK_TEXT_VIEW(oui
->preview
));
366 gtk_text_buffer_get_bounds(buffer
, &start
, &end
);
367 gtk_text_buffer_delete(buffer
, &start
, &end
);
368 gtk_text_buffer_insert_at_cursor(buffer
,
369 "(Note: this is not a real HTML preview because LogJam "
370 "was compiled without HTML support. This widget is in "
371 "here as a placeholder until we figure out what to put " "here.)\n\n", -1);
372 gtk_text_buffer_insert_at_cursor(buffer
, entry
->event
, -1);
374 lj_entry_free(entry
);
379 static void offlineui_init (GTypeInstance
*instance
, gpointer g_class
) {
380 OfflineUI
*oui
= OFFLINEUI(instance
);
381 GtkWidget
*hbox
, *vbox
, *nb
, *copy
;
382 GtkTreeSelection
*sel
;
384 oui
->pb_friends
= gtk_widget_render_icon(GTK_WIDGET(oui
), "logjam-protected", GTK_ICON_SIZE_MENU
, NULL
);
385 oui
->pb_private
= gtk_widget_render_icon(GTK_WIDGET(oui
), "logjam-private", GTK_ICON_SIZE_MENU
, NULL
);
387 gtk_window_set_default_size(GTK_WINDOW(oui
), 400, 500);
388 gtk_window_set_title(GTK_WINDOW(oui
), _("Select Offline Entry"));
390 vbox
= gtk_vbox_new(FALSE
, 6);
391 hbox
= gtk_hbox_new(FALSE
, 6);
393 nb
= gtk_notebook_new();
394 gtk_notebook_append_page(GTK_NOTEBOOK(nb
), make_cal(oui
), gtk_label_new(_("Select by Date")));
395 gtk_notebook_append_page(GTK_NOTEBOOK(nb
), make_search(oui
), gtk_label_new(_("Select by Search")));
397 gtk_box_pack_start(GTK_BOX(hbox
), nb
, FALSE
, FALSE
, 0);
400 oui
->listbox
= gtk_vbox_new(FALSE
, 6);
401 gtk_box_pack_end(GTK_BOX(oui
->listbox
), scroll_wrap(oui
->list
), TRUE
, TRUE
, 0);
402 gtk_box_pack_start(GTK_BOX(hbox
), oui
->listbox
, TRUE
, TRUE
, 0);
404 gtk_box_pack_start(GTK_BOX(vbox
), hbox
, FALSE
, FALSE
, 0);
406 sel
= gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
));
408 oui
->preview
= html_preview_new((GetEntryFunc
) load_selected
, oui
);
409 gtk_box_pack_start(GTK_BOX(vbox
), scroll_wrap(oui
->preview
), TRUE
, TRUE
, 0);
410 g_signal_connect_swapped(G_OBJECT(sel
), "changed", G_CALLBACK(preview_update
), oui
->preview
);
412 oui
->preview
= gtk_text_view_new();
413 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(oui
->preview
), GTK_WRAP_WORD
);
414 gtk_text_view_set_editable(GTK_TEXT_VIEW(oui
->preview
), FALSE
);
415 gtk_box_pack_start(GTK_BOX(vbox
), scroll_wrap(oui
->preview
), TRUE
, TRUE
, 0);
416 g_signal_connect_swapped(G_OBJECT(sel
), "changed", G_CALLBACK(content_update
), oui
);
419 gtk_widget_show_all(vbox
);
420 jam_dialog_set_contents(GTK_DIALOG(oui
), vbox
);
422 copy
= gtk_button_new_from_stock(GTK_STOCK_COPY
);
423 gtk_tooltips_set_tip(app
.tooltips
, copy
,
424 _("Copy online entry URL to clipboard"),
425 _("Clicking here will place the URL of the entry currently "
426 "being previewed in your clipboard, so you can link to it."));
427 gtk_dialog_add_action_widget(GTK_DIALOG(oui
), copy
, LJ_OFFLINE_COPY_URL
);
428 gtk_widget_show(copy
);
430 gtk_dialog_add_buttons(GTK_DIALOG(oui
), GTK_STOCK_JUMP_TO
, LJ_OFFLINE_JUMP_TO
, GTK_STOCK_CANCEL
, GTK_RESPONSE_CANCEL
, GTK_STOCK_OK
, GTK_RESPONSE_OK
, NULL
);
431 /* let the selection-changed watcher update the button sensitivities. */
432 selection_changed_cb(gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
)), oui
);
433 geometry_tie(GTK_WIDGET(oui
), GEOM_OFFLINE
);
437 static void offlineui_finalize (GObject
*object
) {
438 GObjectClass
*parent_class
;
439 OfflineUI
*oui
= OFFLINEUI(object
);
441 journal_store_free(oui
->journalstore
);
442 g_object_unref(oui
->pb_friends
);
443 g_object_unref(oui
->pb_private
);
445 parent_class
= g_type_class_peek_parent(G_OBJECT_GET_CLASS(object
));
446 parent_class
->finalize(object
);
450 static void offlineui_class_init (gpointer klass
, gpointer class_data
) {
451 GObjectClass
*gclass
= G_OBJECT_CLASS(klass
);
452 gclass
->finalize
= offlineui_finalize
;
456 static GType
offlineui_get_type (void) {
457 static GType new_type
= 0;
459 const GTypeInfo new_info
= {
460 sizeof(GtkDialogClass
),
463 (GClassInitFunc
) offlineui_class_init
,
470 new_type
= g_type_register_static(GTK_TYPE_DIALOG
, "OfflineUI", &new_info
, 0);
476 static GtkWidget
*offlineui_new (void) {
477 return GTK_WIDGET(g_object_new(offlineui_get_type(), NULL
));
481 static gchar
*offline_current_entry_link (OfflineUI
*oui
) {
482 /*gint itemid, anum; */
484 JamAccount
*acc
= oui
->account
;
485 LJEntry
*e
= load_selected(oui
);
486 if (e
== NULL
) return NULL
;
487 if (!JAM_ACCOUNT_IS_LJ(acc
)) return NULL
; // XXX: warning?
488 /* XXX: until 1721 closes
489 itemid = lj_entry_get_itemid(e);
490 anum = lj_entry_get_anum(e);
492 url = g_strdup_printf("%s/users/%s/%d.html",
493 jam_account_lj_get_server(acc)->url,
494 jam_account_lj_get_user(acc)->username,
495 itemid *256 + anum); */
498 lj_entry_get_time(e
, &time
);
499 url
= g_strdup_printf("%s/users/%s/%04d/%02d/%02d/",
500 jam_account_lj_get_server(JAM_ACCOUNT_LJ(acc
))->url
,
501 jam_account_lj_get_user(JAM_ACCOUNT_LJ(acc
))->username
, time
.tm_year
+1900, time
.tm_mon
+1, time
.tm_mday
);
507 static void offline_jump_to_online (OfflineUI
*oui
) {
508 gchar
*url
= offline_current_entry_link(oui
);
509 spawn_url(GTK_WINDOW(oui
), url
);
514 static void offline_copy_link (OfflineUI
*oui
) {
515 gchar
*url
= offline_current_entry_link(oui
);
516 if (url
&& *url
!= '\0') gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD
), url
, -1/*determine length automatically*/);
521 LJEntry
*offline_dlg_run (GtkWindow
*parent
, JamAccount
*acc
) {
522 JournalStore
*js
= NULL
;
524 LJEntry
*entry
= NULL
;
527 js
= journal_store_open(acc
, FALSE
, NULL
);
528 if (js
== NULL
&& !check_entries(parent
, acc
)) return NULL
;
530 oui
= offlineui_new();
531 OFFLINEUI(oui
)->journalstore
= js
;
532 OFFLINEUI(oui
)->account
= acc
;
533 month_changed_cb(GTK_CALENDAR(OFFLINEUI(oui
)->cal
), OFFLINEUI(oui
));
534 day_selected_cb(GTK_CALENDAR(OFFLINEUI(oui
)->cal
), OFFLINEUI(oui
));
535 gtk_window_set_transient_for(GTK_WINDOW(oui
), parent
);
537 switch (gtk_dialog_run(GTK_DIALOG(oui
))) {
538 case LJ_OFFLINE_COPY_URL
: offline_copy_link(OFFLINEUI(oui
)); break;
539 case LJ_OFFLINE_JUMP_TO
: offline_jump_to_online(OFFLINEUI(oui
)); break;
540 case GTK_RESPONSE_OK
: entry
= load_selected(OFFLINEUI(oui
)); /* fallthrough */
541 default: run
= FALSE
;
544 gtk_widget_destroy(oui
);