1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2003 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
15 #include "journalstore.h"
22 GtkWidget
*cal
, *searchentry
, *searchcase
;
23 GtkWidget
*summary
, *listbox
, *list
, *preview
;
24 GdkPixbuf
*pb_friends
, *pb_private
;
27 JournalStore
*journalstore
;
30 int current_itemid
; /* itemid of entry being previewed. useful to
31 pop back the calendar after some browsing */
41 static GType
offlineui_get_type(void);
42 #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 update_marks(OfflineUI
*oui
) {
54 gtk_calendar_get_date(GTK_CALENDAR(oui
->cal
), &year
, &mon
, NULL
);
55 days
= journal_store_get_month_entries(oui
->journalstore
, year
, mon
+1);
57 gtk_calendar_clear_marks(GTK_CALENDAR(oui
->cal
));
58 for (day
= 1; day
<= 31; day
++) {
59 if (days
& (1 << day
))
60 gtk_calendar_mark_day(GTK_CALENDAR(oui
->cal
), day
);
65 month_changed_cb(GtkCalendar
*calendar
, OfflineUI
*oui
) {
70 get_entries_cb(int itemid
, time_t etime
, const char *summary
, LJSecurity
*sec
, gpointer data
) {
72 OfflineUI
*oui
= (OfflineUI
*)data
;
76 case LJ_SECURITY_FRIENDS
:
77 case LJ_SECURITY_CUSTOM
:
78 pb
= oui
->pb_friends
; break;
79 case LJ_SECURITY_PRIVATE
:
80 pb
= oui
->pb_private
; break;
82 pb
= NULL
; /* public: no icon */
85 gtk_list_store_append(oui
->store
, &iter
);
86 gtk_list_store_set(oui
->store
, &iter
,
95 load_day(OfflineUI
*oui
) {
97 gtk_calendar_get_date(GTK_CALENDAR(oui
->cal
), &year
, &mon
, &day
);
99 gtk_widget_destroy(oui
->summary
);
102 gtk_list_store_clear(oui
->store
);
103 journal_store_get_day_entries(oui
->journalstore
, year
, mon
+1, day
,
104 get_entries_cb
, oui
);
108 day_selected_cb(GtkCalendar
*calendar
, OfflineUI
*oui
) {
112 if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(oui
->store
), &iter
)) {
113 gtk_tree_selection_select_iter(
114 gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
)),
120 selection_changed_cb(GtkTreeSelection
*ts
, OfflineUI
*oui
) {
123 gboolean sensitive
= gtk_tree_selection_get_selected(ts
, &model
, &iter
);
125 gtk_dialog_set_response_sensitive(GTK_DIALOG(oui
),
126 LJ_OFFLINE_JUMP_TO
, sensitive
);
127 gtk_dialog_set_response_sensitive(GTK_DIALOG(oui
),
128 LJ_OFFLINE_COPY_URL
, sensitive
);
129 gtk_dialog_set_response_sensitive(GTK_DIALOG(oui
),
130 GTK_RESPONSE_OK
, sensitive
);
134 make_list(OfflineUI
*oui
) {
135 GtkCellRenderer
*renderer
;
136 GtkTreeViewColumn
*column
;
137 GtkTreeSelection
*sel
;
139 oui
->store
= gtk_list_store_new(4, G_TYPE_INT
, G_TYPE_LONG
, GDK_TYPE_PIXBUF
, G_TYPE_STRING
);
140 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(oui
->store
),
141 COL_DATE
, GTK_SORT_ASCENDING
);
142 oui
->list
= gtk_tree_view_new_with_model(GTK_TREE_MODEL(oui
->store
));
143 g_object_unref(G_OBJECT(oui
->store
));
145 sel
= gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
));
146 g_signal_connect(G_OBJECT(sel
), "changed",
147 G_CALLBACK(selection_changed_cb
), oui
);
149 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(oui
->list
), FALSE
);
151 column
= gtk_tree_view_column_new();
152 gtk_tree_view_column_set_title(column
, _("Summary"));
153 gtk_tree_view_column_set_sizing(column
, GTK_TREE_VIEW_COLUMN_AUTOSIZE
);
155 renderer
= gtk_cell_renderer_pixbuf_new();
156 gtk_tree_view_column_pack_start(column
, renderer
, FALSE
);
157 gtk_tree_view_column_add_attribute(column
, renderer
, "pixbuf", COL_SECURITY
);
159 renderer
= gtk_cell_renderer_text_new();
160 gtk_tree_view_column_pack_start(column
, renderer
, TRUE
);
161 gtk_tree_view_column_add_attribute(column
, renderer
, "text", COL_SUMMARY
);
163 gtk_tree_view_column_set_sort_column_id(column
, COL_SUMMARY
);
164 gtk_tree_view_append_column(GTK_TREE_VIEW(oui
->list
), column
);
168 get_selected_itemid(OfflineUI
*oui
) {
172 if (!gtk_tree_selection_get_selected(
173 gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
)),
176 gtk_tree_model_get(model
, &iter
,
183 load_selected(OfflineUI
*oui
) {
187 itemid
= get_selected_itemid(oui
);
190 entry
= journal_store_get_entry(oui
->journalstore
, itemid
);
192 g_warning("unable to find entry %d\n", itemid
);
197 select_entry(OfflineUI
*oui
, int itemid
) {
200 guint year
, mon
, day
;
202 GtkTreeModel
*model
= GTK_TREE_MODEL(oui
->store
);
203 GtkTreeSelection
*sel
= gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
));
206 etime
= journal_store_lookup_entry_time(oui
->journalstore
, itemid
);
208 etm
= gmtime(&etime
);
209 gtk_calendar_get_date(GTK_CALENDAR(oui
->cal
), &year
, &mon
, &day
);
211 gtk_calendar_freeze(GTK_CALENDAR(oui
->cal
));
212 g_signal_handlers_block_matched(oui
->cal
, G_SIGNAL_MATCH_DATA
,
213 0, 0, NULL
, NULL
, oui
);
215 /* the calendar gets confused in situations like switching into
216 * a 30-day month and have day 31 selected.
217 * so we move to the middle, switch months, and then move to the right place. */
218 if (day
!= etm
->tm_mday
)
219 gtk_calendar_select_day(GTK_CALENDAR(oui
->cal
), 15);
220 if (year
!= etm
->tm_year
+1900 || mon
!= etm
->tm_mon
)
221 gtk_calendar_select_month(GTK_CALENDAR(oui
->cal
),
222 etm
->tm_mon
, etm
->tm_year
+1900);
223 if (day
!= etm
->tm_mday
)
224 gtk_calendar_select_day(GTK_CALENDAR(oui
->cal
), etm
->tm_mday
);
226 if (day
!= etm
->tm_mday
)
228 if (year
!= etm
->tm_year
+1900 || mon
!= etm
->tm_mon
)
231 g_signal_handlers_unblock_matched(oui
->cal
, G_SIGNAL_MATCH_DATA
,
232 0, 0, NULL
, NULL
, oui
);
233 gtk_calendar_thaw(GTK_CALENDAR(oui
->cal
));
235 if (gtk_tree_model_get_iter_first(model
, &iter
)) {
237 gtk_tree_model_get(model
, &iter
, COL_ITEMID
, &fitemid
, -1);
238 if (fitemid
== itemid
) {
239 gtk_tree_selection_select_iter(sel
, &iter
);
242 } while (gtk_tree_model_iter_next(model
, &iter
));
246 /* move the selection to the nearest next or previous item. The present
247 * selection need not be an actual entry. */
249 move_relative(OfflineUI
*oui
, int dir
) {
250 gboolean found
= FALSE
;
251 int itemid
= get_selected_itemid(oui
);
253 /* selection exists */
254 found
= journal_store_find_relative(oui
->journalstore
, itemid
, &itemid
, dir
, NULL
);
256 /* some more cajoling necessary */
257 guint year
, month
, day
;
258 struct tm when_tm
= {0};
259 gtk_calendar_get_date(GTK_CALENDAR(oui
->cal
), &year
, &month
, &day
);
260 when_tm
.tm_year
= year
-1900;
261 when_tm
.tm_mon
= month
;
262 when_tm
.tm_mday
= day
;
263 found
= journal_store_find_relative_by_time(oui
->journalstore
, lj_timegm(&when_tm
), &itemid
, dir
, NULL
);
265 if (found
) select_entry(oui
, itemid
);
269 prev_cb(GtkWidget
*button
, OfflineUI
*oui
) {
270 move_relative(oui
, -1);
274 next_cb(GtkWidget
*button
, OfflineUI
*oui
) {
275 move_relative(oui
, 1);
279 make_cal(OfflineUI
*oui
) {
280 GtkWidget
*calbox
, *bbox
, *next
, *prev
;
282 calbox
= gtk_vbox_new(FALSE
, 6);
283 gtk_container_set_border_width(GTK_CONTAINER(calbox
), 6);
285 oui
->cal
= gtk_calendar_new();
286 gtk_calendar_display_options(GTK_CALENDAR(oui
->cal
),
287 GTK_CALENDAR_SHOW_HEADING
);
289 g_signal_connect(G_OBJECT(oui
->cal
), "month_changed",
290 G_CALLBACK(month_changed_cb
), oui
);
291 g_signal_connect(G_OBJECT(oui
->cal
), "day_selected",
292 G_CALLBACK(day_selected_cb
), oui
);
293 gtk_box_pack_start(GTK_BOX(calbox
), oui
->cal
, FALSE
, FALSE
, 0);
295 bbox
= gtk_hbutton_box_new();
296 gtk_button_box_set_layout(GTK_BUTTON_BOX(bbox
), GTK_BUTTONBOX_EDGE
);
297 gtk_box_set_spacing(GTK_BOX(bbox
), 6);
298 prev
= gtk_button_new_from_stock(GTK_STOCK_GO_BACK
);
299 g_signal_connect(G_OBJECT(prev
), "clicked",
300 G_CALLBACK(prev_cb
), oui
);
301 gtk_box_pack_start(GTK_BOX(bbox
), prev
, FALSE
, FALSE
, 0);
302 next
= gtk_button_new_from_stock(GTK_STOCK_GO_FORWARD
);
303 g_signal_connect(G_OBJECT(next
), "clicked",
304 G_CALLBACK(next_cb
), oui
);
305 gtk_box_pack_start(GTK_BOX(bbox
), next
, FALSE
, FALSE
, 0);
307 gtk_box_pack_start(GTK_BOX(calbox
), bbox
, FALSE
, FALSE
, 0);
313 search_case_cb(const char *str
, gpointer data
) {
316 if (!str
) return FALSE
;
317 down
= g_utf8_strdown(str
, -1);
318 ret
= (strstr(down
, data
) != NULL
);
324 search_cb(const char *str
, gpointer data
) {
325 return str
&& strstr(str
, data
) != NULL
;
329 find_cb(GtkWidget
*button
, OfflineUI
*oui
) {
333 search
= gtk_entry_get_text(GTK_ENTRY(oui
->searchentry
));
335 gtk_list_store_clear(oui
->store
);
336 if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(oui
->searchcase
))) {
337 journal_store_scan(oui
->journalstore
,
338 search_cb
, (gpointer
)search
,
339 get_entries_cb
, oui
);
341 char *lsearch
= g_utf8_strdown(search
, -1);
342 journal_store_scan(oui
->journalstore
,
343 search_case_cb
, lsearch
,
344 get_entries_cb
, oui
);
348 summary
= g_strdup_printf(_("Search found %d entries:"),
349 gtk_tree_model_iter_n_children(GTK_TREE_MODEL(oui
->store
), NULL
));
351 oui
->summary
= gtk_label_new(summary
);
352 gtk_widget_show(oui
->summary
);
353 gtk_box_pack_start(GTK_BOX(oui
->listbox
), oui
->summary
,
356 gtk_label_set_text(GTK_LABEL(oui
->summary
), summary
);
362 make_search(OfflineUI
*oui
) {
363 GtkWidget
*box
, *l
, *bbox
, *button
;
365 box
= gtk_vbox_new(FALSE
, 6);
366 gtk_container_set_border_width(GTK_CONTAINER(box
), 6);
368 l
= gtk_label_new_with_mnemonic(_("_Search:"));
369 gtk_misc_set_alignment(GTK_MISC(l
), 0, 0.5);
370 gtk_box_pack_start(GTK_BOX(box
), l
, FALSE
, FALSE
, 0);
371 oui
->searchentry
= gtk_entry_new();
372 gtk_box_pack_start(GTK_BOX(box
), oui
->searchentry
, FALSE
, FALSE
, 0);
373 gtk_label_set_mnemonic_widget(GTK_LABEL(l
), oui
->searchentry
);
375 oui
->searchcase
= gtk_check_button_new_with_mnemonic(_("_Case sensitive"));
376 gtk_box_pack_start(GTK_BOX(box
), oui
->searchcase
, FALSE
, FALSE
, 0);
378 bbox
= gtk_hbutton_box_new();
379 gtk_button_box_set_layout(GTK_BUTTON_BOX(bbox
), GTK_BUTTONBOX_END
);
380 button
= gtk_button_new_from_stock(GTK_STOCK_FIND
);
381 g_signal_connect(G_OBJECT(button
), "clicked",
382 G_CALLBACK(find_cb
), oui
);
383 g_signal_connect(G_OBJECT(oui
->searchentry
), "activate",
384 G_CALLBACK(find_cb
), oui
);
385 gtk_box_pack_start(GTK_BOX(bbox
), button
, FALSE
, FALSE
, 0);
386 gtk_box_pack_start(GTK_BOX(box
), bbox
, FALSE
, FALSE
, 0);
392 check_entries(GtkWindow
*parent
, JamAccount
*acc
) {
396 dlg
= gtk_message_dialog_new(parent
, GTK_DIALOG_MODAL
,
397 GTK_MESSAGE_INFO
, GTK_BUTTONS_NONE
,
398 _("LogJam has no offline entries for this journal. "
399 "You must first synchronize this journal to your local "
400 "storage before you can load entries from it."));
401 gtk_dialog_add_buttons(GTK_DIALOG(dlg
),
402 _("_Synchronize"), GTK_RESPONSE_OK
,
403 GTK_STOCK_CANCEL
, GTK_RESPONSE_CANCEL
,
405 res
= gtk_dialog_run(GTK_DIALOG(dlg
));
406 gtk_widget_destroy(dlg
);
407 if (res
== GTK_RESPONSE_OK
)
408 return sync_run(JAM_ACCOUNT_LJ(acc
), parent
);
414 content_update(OfflineUI
*oui
) {
416 GtkTextBuffer
*buffer
;
417 GtkTextIter start
, end
;
419 entry
= load_selected(oui
);
422 buffer
= gtk_text_view_get_buffer(GTK_TEXT_VIEW(oui
->preview
));
423 gtk_text_buffer_get_bounds(buffer
, &start
, &end
);
424 gtk_text_buffer_delete(buffer
, &start
, &end
);
425 gtk_text_buffer_insert_at_cursor(buffer
,
426 "(Note: this is not a real HTML preview because LogJam "
427 "was compiled without HTML support. This widget is in "
428 "here as a placeholder until we figure out what to put "
430 gtk_text_buffer_insert_at_cursor(buffer
, entry
->event
, -1);
432 lj_entry_free(entry
);
437 offlineui_init(GTypeInstance
*instance
, gpointer g_class
) {
438 OfflineUI
*oui
= OFFLINEUI(instance
);
439 GtkWidget
*hbox
, *vbox
, *nb
, *copy
;
440 GtkTreeSelection
*sel
;
442 oui
->pb_friends
= gtk_widget_render_icon(GTK_WIDGET(oui
),
443 "logjam-protected", GTK_ICON_SIZE_MENU
, NULL
);
444 oui
->pb_private
= gtk_widget_render_icon(GTK_WIDGET(oui
),
445 "logjam-private", GTK_ICON_SIZE_MENU
, NULL
);
447 gtk_window_set_default_size(GTK_WINDOW(oui
), 400, 500);
448 gtk_window_set_title(GTK_WINDOW(oui
), _("Select Offline Entry"));
450 vbox
= gtk_vbox_new(FALSE
, 6);
451 hbox
= gtk_hbox_new(FALSE
, 6);
453 nb
= gtk_notebook_new();
454 gtk_notebook_append_page(GTK_NOTEBOOK(nb
), make_cal(oui
),
455 gtk_label_new(_("Select by Date")));
456 gtk_notebook_append_page(GTK_NOTEBOOK(nb
), make_search(oui
),
457 gtk_label_new(_("Select by Search")));
459 gtk_box_pack_start(GTK_BOX(hbox
), nb
, FALSE
, FALSE
, 0);
462 oui
->listbox
= gtk_vbox_new(FALSE
, 6);
463 gtk_box_pack_end(GTK_BOX(oui
->listbox
), scroll_wrap(oui
->list
),
465 gtk_box_pack_start(GTK_BOX(hbox
), oui
->listbox
, TRUE
, TRUE
, 0);
467 gtk_box_pack_start(GTK_BOX(vbox
), hbox
, FALSE
, FALSE
, 0);
469 sel
= gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
));
471 oui
->preview
= html_preview_new((GetEntryFunc
)load_selected
, oui
);
472 gtk_box_pack_start(GTK_BOX(vbox
), scroll_wrap(oui
->preview
), TRUE
, TRUE
, 0);
473 g_signal_connect_swapped(G_OBJECT(sel
), "changed",
474 G_CALLBACK(preview_update
), oui
->preview
);
476 oui
->preview
= gtk_text_view_new();
477 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(oui
->preview
), GTK_WRAP_WORD
);
478 gtk_text_view_set_editable(GTK_TEXT_VIEW(oui
->preview
), FALSE
);
479 gtk_box_pack_start(GTK_BOX(vbox
), scroll_wrap(oui
->preview
), TRUE
, TRUE
, 0);
480 g_signal_connect_swapped(G_OBJECT(sel
), "changed",
481 G_CALLBACK(content_update
), oui
);
484 gtk_widget_show_all(vbox
);
485 jam_dialog_set_contents(GTK_DIALOG(oui
), vbox
);
487 copy
= gtk_button_new_from_stock(GTK_STOCK_COPY
);
488 gtk_tooltips_set_tip(app
.tooltips
, copy
,
489 _("Copy online entry URL to clipboard"),
490 _("Clicking here will place the URL of the entry currently "
491 "being previewed in your clipboard, so you can link to it."));
492 gtk_dialog_add_action_widget(GTK_DIALOG(oui
),
493 copy
, LJ_OFFLINE_COPY_URL
);
494 gtk_widget_show(copy
);
496 gtk_dialog_add_buttons(GTK_DIALOG(oui
),
497 GTK_STOCK_JUMP_TO
, LJ_OFFLINE_JUMP_TO
,
498 GTK_STOCK_CANCEL
, GTK_RESPONSE_CANCEL
,
499 GTK_STOCK_OK
, GTK_RESPONSE_OK
,
501 /* let the selection-changed watcher update the button sensitivities. */
502 selection_changed_cb(gtk_tree_view_get_selection(GTK_TREE_VIEW(oui
->list
)),
504 geometry_tie(GTK_WIDGET(oui
), GEOM_OFFLINE
);
508 offlineui_finalize(GObject
*object
) {
509 GObjectClass
*parent_class
;
510 OfflineUI
*oui
= OFFLINEUI(object
);
512 journal_store_free(oui
->journalstore
);
513 g_object_unref(oui
->pb_friends
);
514 g_object_unref(oui
->pb_private
);
516 parent_class
= g_type_class_peek_parent(G_OBJECT_GET_CLASS(object
));
517 parent_class
->finalize(object
);
521 offlineui_class_init(gpointer klass
, gpointer class_data
) {
522 GObjectClass
*gclass
= G_OBJECT_CLASS(klass
);
524 gclass
->finalize
= offlineui_finalize
;
528 offlineui_get_type(void) {
529 static GType new_type
= 0;
531 const GTypeInfo new_info
= {
532 sizeof(GtkDialogClass
),
535 (GClassInitFunc
) offlineui_class_init
,
542 new_type
= g_type_register_static(GTK_TYPE_DIALOG
,
543 "OfflineUI", &new_info
, 0);
549 offlineui_new(void) {
550 return GTK_WIDGET(g_object_new(offlineui_get_type(), NULL
));
554 offline_current_entry_link(OfflineUI
*oui
) {
555 /*gint itemid, anum;*/
557 JamAccount
*acc
= oui
->account
;
558 LJEntry
*e
= load_selected(oui
);
563 if (! JAM_ACCOUNT_IS_LJ(acc
))
564 return NULL
; // XXX: warning?
566 /* XXX: until 1721 closes
567 itemid = lj_entry_get_itemid(e);
568 anum = lj_entry_get_anum(e);
570 url = g_strdup_printf("%s/users/%s/%d.html",
571 jam_account_lj_get_server(acc)->url,
572 jam_account_lj_get_user(acc)->username,
573 itemid * 256 + anum); */
576 lj_entry_get_time(e
, &time
);
577 url
= g_strdup_printf("%s/users/%s/%04d/%02d/%02d/",
578 jam_account_lj_get_server(JAM_ACCOUNT_LJ(acc
))->url
,
579 jam_account_lj_get_user(JAM_ACCOUNT_LJ(acc
))->username
,
589 offline_jump_to_online(OfflineUI
*oui
) {
590 gchar
*url
= offline_current_entry_link(oui
);
591 spawn_url(GTK_WINDOW(oui
), url
);
596 offline_copy_link(OfflineUI
*oui
) {
597 gchar
*url
= offline_current_entry_link(oui
);
598 if (url
&& *url
!= '\0')
599 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD
),
600 url
, -1 /* determine length automatically */);
605 offline_dlg_run(GtkWindow
*parent
, JamAccount
*acc
) {
606 JournalStore
*js
= NULL
;
608 LJEntry
*entry
= NULL
;
612 js
= journal_store_open(acc
, FALSE
, NULL
);
614 if (!check_entries(parent
, acc
))
618 oui
= offlineui_new();
619 OFFLINEUI(oui
)->journalstore
= js
;
620 OFFLINEUI(oui
)->account
= acc
;
621 month_changed_cb(GTK_CALENDAR(OFFLINEUI(oui
)->cal
), OFFLINEUI(oui
));
622 day_selected_cb (GTK_CALENDAR(OFFLINEUI(oui
)->cal
), OFFLINEUI(oui
));
623 gtk_window_set_transient_for(GTK_WINDOW(oui
), parent
);
626 switch (gtk_dialog_run(GTK_DIALOG(oui
))) {
627 case LJ_OFFLINE_COPY_URL
:
628 offline_copy_link(OFFLINEUI(oui
));
630 case LJ_OFFLINE_JUMP_TO
:
631 offline_jump_to_online(OFFLINEUI(oui
));
633 case GTK_RESPONSE_OK
:
634 entry
= load_selected(OFFLINEUI(oui
));
641 gtk_widget_destroy(oui
);