Write.as GTK desktop app https://write.as/apps/desktop
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

251 lines
9.2 KiB

  1. public class WriteAs.MainWindow : Gtk.ApplicationWindow {
  2. private Gtk.TextView canvas;
  3. private bool dark_mode = false;
  4. private string font = "Lora, 'Palatino Linotype',"
  5. + "'Book Antiqua', 'New York', 'DejaVu serif', serif";
  6. private string fontstyle = "serif";
  7. construct {
  8. construct_toolbar();
  9. build_keyboard_shortcuts();
  10. var scrolled = new Gtk.ScrolledWindow(null, null);
  11. canvas = new Gtk.TextView();
  12. canvas.wrap_mode = Gtk.WrapMode.WORD_CHAR;
  13. scrolled.add(canvas);
  14. add(scrolled);
  15. var text_changed = false;
  16. canvas.event_after.connect((evt) => {
  17. // TODO This word count algorithm may be quite naive
  18. // and could do improvement.
  19. var word_count = canvas.buffer.text.split(" ").length;
  20. title = ngettext("%i word","%i words",word_count).printf(word_count);
  21. text_changed = true;
  22. });
  23. Timeout.add_full(Priority.DEFAULT_IDLE, 100/*ms*/, () => {
  24. if (!text_changed) return Source.CONTINUE;
  25. try {
  26. draft_file().replace_contents(canvas.buffer.text.data, null, false,
  27. FileCreateFlags.PRIVATE | FileCreateFlags.REPLACE_DESTINATION,
  28. null);
  29. text_changed = false;
  30. } catch (Error err) {/* We'll try again anyways. */}
  31. return Source.CONTINUE;
  32. });
  33. adjust_text_style();
  34. }
  35. public MainWindow(Gtk.Application app) {
  36. set_application(app);
  37. try {
  38. open_file(draft_file());
  39. } catch (Error err) {/* It's fine... */}
  40. set_default_size(800, 600);
  41. }
  42. private static File draft_file() {
  43. var home = File.new_for_path(Environment.get_home_dir());
  44. return home.get_child(".writeas-draft.txt");
  45. }
  46. private void construct_toolbar() {
  47. var header = new Gtk.HeaderBar();
  48. header.show_close_button = true;
  49. set_titlebar(header);
  50. var publish_button = new Gtk.Button.from_icon_name("document-send",
  51. Gtk.IconSize.SMALL_TOOLBAR);
  52. publish_button.clicked.connect(() => {
  53. title = _("Publishing post…");
  54. canvas.sensitive = false;
  55. publish.begin((obj, res) => {
  56. canvas.buffer.text = publish.end(res);
  57. canvas.sensitive = true;
  58. });
  59. });
  60. header.pack_end(publish_button);
  61. var darkmode_button = new Gtk.ToggleButton();
  62. darkmode_button.tooltip_text = _("Toggle dark theme");
  63. // NOTE the fallback icon is a bit of a meaning stretch, but it works.
  64. var icon_theme = Gtk.IconTheme.get_default();
  65. darkmode_button.image = new Gtk.Image.from_icon_name(
  66. icon_theme.has_icon("writeas-bright-dark") ?
  67. "writeas-bright-dark" : "weather-clear-night",
  68. Gtk.IconSize.SMALL_TOOLBAR);
  69. darkmode_button.draw_indicator = false;
  70. var settings = Gtk.Settings.get_default();
  71. darkmode_button.toggled.connect(() => {
  72. settings.gtk_application_prefer_dark_theme = darkmode_button.active;
  73. dark_mode = darkmode_button.active;
  74. adjust_text_style();
  75. });
  76. header.pack_end(darkmode_button);
  77. var fonts = new Gtk.MenuButton();
  78. fonts.tooltip_text = _("Change document font");
  79. fonts.image = new Gtk.Image.from_icon_name("font-x-generic", Gtk.IconSize.SMALL_TOOLBAR);
  80. fonts.popup = new Gtk.Menu();
  81. header.pack_start(fonts);
  82. build_fontoption(fonts.popup, _("Serif"), "serif", font);
  83. build_fontoption(fonts.popup, _("Sans-serif"), "sans",
  84. "'Open Sans', 'Segoe UI', Tahoma, Arial, sans-serif");
  85. build_fontoption(fonts.popup, _("Monospace"), "mono", "Hack, consolas," +
  86. "Menlo-Regular, Menlo, Monaco, 'ubuntu mono', monospace");
  87. fonts.popup.show_all();
  88. }
  89. private void build_fontoption(Gtk.Menu menu,
  90. string label, string fontstyle, string families) {
  91. var option = new Gtk.MenuItem.with_label(label);
  92. option.activate.connect(() => {
  93. this.font = families;
  94. this.fontstyle = fontstyle;
  95. adjust_text_style();
  96. });
  97. var styles = option.get_style_context();
  98. var provider = new Gtk.CssProvider();
  99. try {
  100. provider.load_from_data("* {font: %s;}".printf(families));
  101. styles.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
  102. } catch (Error e) {
  103. warning(e.message);
  104. }
  105. menu.add(option);
  106. }
  107. private Gtk.CssProvider cur_styles = null;
  108. private void adjust_text_style() {
  109. try {
  110. var styles = canvas.get_style_context();
  111. if (cur_styles != null) styles.remove_provider(cur_styles);
  112. var css = "* {font: %s; padding: 20px;}".printf(font);
  113. if (dark_mode) {
  114. // Try to detect whether the system provided a better dark mode.
  115. var text_color = styles.get_color(Gtk.StateFlags.ACTIVE);
  116. double h, s, v;
  117. Gtk.rgb_to_hsv(text_color.red, text_color.green, text_color.blue,
  118. out h, out s, out v);
  119. if (v < 0.5) css += "* {background: black; color: white;}";
  120. }
  121. cur_styles = new Gtk.CssProvider();
  122. cur_styles.load_from_data(css);
  123. styles.add_provider(cur_styles, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
  124. } catch (Error e) {
  125. warning(e.message);
  126. }
  127. }
  128. private async string publish() {
  129. var session = new Soup.Session();
  130. // Send the request
  131. var req = new Soup.Message("POST", "https://write.as/api/posts");
  132. // TODO specify font.
  133. var req_body = "{\"body\": \"%s\", \"font\": \"%s\"}".printf(
  134. canvas.buffer.text, fontstyle);
  135. req.set_request("application/json", Soup.MemoryUse.COPY, req_body.data);
  136. try {
  137. var resp = yield session.send_async(req);
  138. // Handle the response
  139. if (req.status_code != 201)
  140. return _("Error code: HTTP %u").printf(req.status_code);
  141. var json = new Json.Parser();
  142. json.load_from_stream(resp);
  143. var data = json.get_root().get_object().get_object_member("data");
  144. var url = "https://write.as/" + data.get_string_member("id");
  145. Gtk.Clipboard.get_default(get_display()).set_text(url, -1);
  146. // Open it in the browser
  147. var browser = AppInfo.get_default_for_uri_scheme("https");
  148. var urls = new List<string>();
  149. urls.append(url);
  150. browser.launch_uris(urls, null);
  151. return _("The link to your published article has been copied into your clipboard for you.");
  152. } catch (Error err) {
  153. return _("Failed to upload post! Are you connected to the Internet?")
  154. + "\n\n" + err.message;
  155. }
  156. }
  157. /* --- */
  158. private void build_keyboard_shortcuts() {
  159. /* These operations are not exposed to the UI as buttons,
  160. as most people are very familiar with them and they are not the
  161. focus of this app. */
  162. var accels = new Gtk.AccelGroup();
  163. accels.connect(Gdk.Key.S, Gdk.ModifierType.CONTROL_MASK,
  164. Gtk.AccelFlags.VISIBLE | Gtk.AccelFlags.LOCKED,
  165. (g,a,k,m) => save_as());
  166. accels.connect(Gdk.Key.S,
  167. Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK,
  168. Gtk.AccelFlags.VISIBLE | Gtk.AccelFlags.LOCKED,
  169. (g,a,k,m) => save_as());
  170. accels.connect(Gdk.Key.O, Gdk.ModifierType.CONTROL_MASK,
  171. Gtk.AccelFlags.VISIBLE | Gtk.AccelFlags.LOCKED, (g, a, k, m) => {
  172. try {
  173. open_file(prompt_file(Gtk.FileChooserAction.OPEN, _("_Open")));
  174. } catch (Error e) {
  175. // It's fine...
  176. }
  177. return true;
  178. });
  179. add_accel_group(accels);
  180. }
  181. private bool save_as() {
  182. try {
  183. var file = prompt_file(Gtk.FileChooserAction.SAVE, _("_Save as"));
  184. file.replace_contents(canvas.buffer.text.data, null, false,
  185. FileCreateFlags.PRIVATE | FileCreateFlags.REPLACE_DESTINATION,
  186. null);
  187. } catch (Error e) {
  188. // It's fine...
  189. }
  190. return true;
  191. }
  192. private File prompt_file(Gtk.FileChooserAction mode, string action)
  193. throws UserCancellable {
  194. var file_chooser = new Gtk.FileChooserDialog(action, this, mode,
  195. _("_Cancel"), Gtk.ResponseType.CANCEL,
  196. action, Gtk.ResponseType.ACCEPT);
  197. file_chooser.select_multiple = false;
  198. var filter = new Gtk.FileFilter();
  199. filter.add_mime_type("text/plain");
  200. file_chooser.set_filter(filter);
  201. var resp = file_chooser.run();
  202. file_chooser.close();
  203. if (resp == Gtk.ResponseType.ACCEPT) return file_chooser.get_file();
  204. else throw new UserCancellable.USER_CANCELLED("FileChooserDialog");
  205. }
  206. public void open_file(File file) throws Error {
  207. uint8[] text;
  208. file.load_contents(null, out text, null);
  209. canvas.buffer.text = (string) text;
  210. }
  211. }
  212. errordomain WriteAs.UserCancellable {USER_CANCELLED}