A fast, pretty, and ready translation app using DeepL free and paid API
@@ -49,8 +49,8 @@Touchups and enhancements
+🚀 1.0.0 Initial release!
diff --git a/data/meson.build b/data/meson.build index 4b2638b..77a4c82 100644 --- a/data/meson.build +++ b/data/meson.build @@ -1,45 +1,104 @@ + +gschema_conf = configuration_data() +gschema_conf.set('APP_ID', app_id) +gschema_file = configure_file( + input: 'inscriptions.gschema.xml.in', + output: '@0@.gschema.xml'.format(app_id), + configuration: gschema_conf, +) + install_data( - 'inscriptions.gschema.xml', + gschema_file, install_dir: get_option('datadir') / 'glib-2.0' / 'schemas', - rename: meson.project_name() + '.gschema.xml' +) + +compile_schemas = find_program('glib-compile-schemas', required: false, disabler: true) +test('Validate schema file', +compile_schemas, +args: ['--strict', '--dry-run', meson.current_source_dir()], ) # metainfo confuses windows, and icons are if not windows_build - icon_sizes = ['16', '32', '48', '64', '128'] + # Install the standard icons in standard build + if not get_option('development') + + icon_sizes = ['16', '32', '48', '64', '128'] + + foreach i : icon_sizes + install_data( + 'icons' / 'hicolor' / i + '.png', + install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i / 'apps', + rename: app_id + '.png' + ) + install_data( + 'icons' / 'hicolor@2' / i + '@2.png', + install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i + '@2' / 'apps', + rename: app_id + '.png' + ) + endforeach - foreach i : icon_sizes - install_data( - 'icons' / 'hicolor' / i + '.png', - install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i / 'apps', - rename: meson.project_name() + '.png' + # Install the dev icon in development builds + else + install_data( + 'icons' / 'dev.svg', + install_dir: get_option('datadir') / 'icons' / 'hicolor' / 'scalable' / 'apps', + rename: app_id + '.svg' + ) + endif + + + # Inject some variables into the desktop file before merging in the translations + desktop_conf = configuration_data() + desktop_conf.set('APP_NAME', app_name) + desktop_conf.set('APP_ID', app_id) + desktop_file_in = configure_file( + input: 'inscriptions.desktop.in.in', + output: '@0@.desktop.in'.format(app_id), + configuration: desktop_conf, ) - install_data( - 'icons' / 'hicolor@2' / i + '@2.png', - install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i + '@2' / 'apps', - rename: meson.project_name() + '.png' + + desktop_file = i18n.merge_file( + input: desktop_file_in, + output: '@0@.desktop'.format(app_id), + po_dir: meson.project_source_root() / 'po', + type: 'desktop', + install: true, + install_dir: get_option('datadir') / 'applications', ) - endforeach + # Inject some variables into the metainfo file before merging in the translations + appstream_conf = configuration_data() + appstream_conf.set('APP_ID', app_id) + appstream_conf.set('GETTEXT_PACKAGE', meson.project_name()) + appstream_file_in = configure_file( + input: 'inscriptions.metainfo.xml.in.in', + output: '@0@.metainfo.xml.in'.format(app_id), + configuration: appstream_conf, + ) - i18n.merge_file( - input: 'inscriptions.desktop.in', - output: meson.project_name() + '.desktop', + appstream_file = i18n.merge_file( + input: appstream_file_in, + output: '@0@.metainfo.xml'.format(app_id), po_dir: meson.project_source_root() / 'po', - type: 'desktop', install: true, - install_dir: get_option('datadir') / 'applications' + install_dir: get_option('datadir') / 'metainfo', ) - i18n.merge_file( - input: 'inscriptions.metainfo.xml.in', - output: meson.project_name() + '.metainfo.xml', - po_dir: meson.project_source_root() / 'po', - install: true, - install_dir: get_option('datadir') / 'metainfo' + # Test definitions + desktop_utils = find_program('desktop-file-validate', required: false) + if desktop_utils.found() + test('Validate desktop file', desktop_utils, args: [desktop_file]) + endif + + appstreamcli = find_program('appstreamcli', required: false, disabler: true) + test('Validate appstream file', + appstreamcli, + args: ['validate', '--no-net', '--explain', appstream_file], ) + endif \ No newline at end of file diff --git a/io.github.elly_code.inscriptions.devel.yml b/io.github.elly_code.inscriptions.devel.yml new file mode 100644 index 0000000..214ca50 --- /dev/null +++ b/io.github.elly_code.inscriptions.devel.yml @@ -0,0 +1,43 @@ +# +# This is danger +# +id: io.github.elly_code.inscriptions.devel +# elementary SDK is not available on Flathub, so use the elementary BaseApp instead +base: io.elementary.BaseApp +base-version: 'circe-25.08' +runtime: org.gnome.Platform +runtime-version: '49' +sdk: org.gnome.Sdk +command: io.github.elly_code.inscriptions.devel + +tags: ['devel'] +desktop-file-name-suffix: ' (Development)' + +finish-args: + - '--share=ipc' + - '--device=dri' + - '--socket=fallback-x11' + - '--socket=wayland' + # Required for communication with DeepL API + - '--share=network' + +cleanup: + - '/include' + - '/lib/pkgconfig' + - '/man' + - '/share/doc' + - '/share/gtk-doc' + - '/share/man' + - '/share/pkgconfig' + - '/share/installed-tests' + - '*.la' + - '*.a' + +modules: + - name: inscriptions + buildsystem: meson + config-opts: + - -Ddevelopment=true + sources: + - type: dir + path: . \ No newline at end of file diff --git a/meson.build b/meson.build index 261f8e4..97c53e9 100644 --- a/meson.build +++ b/meson.build @@ -2,20 +2,33 @@ project( 'io.github.elly_code.inscriptions', 'vala', 'c', - version: '1.0.0' + version: '1.1.0' ) +app_name = 'Inscriptions' +app_id = meson.project_name() +app_version = meson.project_version() + #================================ # Include Gnome and the translations module gnome = import('gnome') i18n = import('i18n') -# Set our translation domain -add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (meson.project_name()), language:'c') -windows_build = build_machine.system() == 'windows' vala_flags = [] +if get_option('development') + app_id += '.devel' + vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip() + app_version += '-@0@'.format (vcs_tag) + vala_flags += ['--define', 'DEVEL'] + +endif + +# Set our translation domain +add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (app_id), language:'c') + # Windows needs icons, special arguments, and skip libportal +windows_build = build_machine.system() == 'windows' if windows_build vala_flags += ['--define', 'WINDOWS'] endif @@ -27,7 +40,11 @@ add_project_arguments(vala_flags, language: 'vala') config_data = configuration_data() config_data.set('version', meson.project_version()) config_data.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir')) -config_data.set_quoted('GETTEXT_PACKAGE', meson.project_name()) +config_data.set_quoted('GETTEXT_PACKAGE', app_id) +config_data.set_quoted('APP_NAME', app_name) +config_data.set_quoted('APP_ID', app_id) +config_data.set_quoted('APP_VERSION', app_version) + config_file = configure_file( input: 'src/Config.vala.in', output: '@BASENAME@', diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..57ae49d --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('development', type: 'boolean', value: false, description: 'If this is a development build') \ No newline at end of file diff --git a/po/POTFILES b/po/POTFILES index 0c8f812..17e6b60 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -2,6 +2,7 @@ src/Windows/MainWindow.vala src/Views/TranslationView.vala src/Widgets/HeaderBar.vala src/Views/ErrorView.vala +src/Enums/StatusCode.vala src/Widgets/ErrorBonusBox.vala src/Views/LogView.vala src/Widgets/PopoverWidgets/ApiEntry.vala diff --git a/po/meson.build b/po/meson.build index d148a69..4d77e2e 100644 --- a/po/meson.build +++ b/po/meson.build @@ -1,4 +1,4 @@ -i18n.gettext(meson.project_name(), +i18n.gettext(app_id, args: '--directory=' + meson.project_source_root(), preset: 'glib' ) diff --git a/src/Config.vala.in b/src/Config.vala.in index a3c7bd4..5390828 100644 --- a/src/Config.vala.in +++ b/src/Config.vala.in @@ -1,2 +1,5 @@ public const string GETTEXT_PACKAGE = @GETTEXT_PACKAGE@; -public const string LOCALEDIR = @LOCALEDIR@; \ No newline at end of file +public const string LOCALEDIR = @LOCALEDIR@; +public const string APP_NAME = @APP_NAME@; +public const string APP_ID = @APP_ID@; +public const string APP_VERSION = @APP_VERSION@; \ No newline at end of file diff --git a/src/Constants.vala b/src/Constants.vala index a238eca..868a376 100644 --- a/src/Constants.vala +++ b/src/Constants.vala @@ -6,7 +6,12 @@ namespace Inscriptions { // Alkrjnlgjrt - public const string RDNN = "io.github.elly_code.inscriptions"; +#if DEVEL + public const string RDNN = "io.github.elly_code.inscriptions.devel"; +#else + public const string RDNN = "io.github.elly_code.inscriptions"; +#endif + public const string DONATE_LINK = "https://ko-fi.com/teamcons/tip"; public const string LINK = "https://www.deepl.com/your-account/keys"; public const int DEBOUNCE_INTERVAL = 1250; // ms @@ -94,15 +99,15 @@ namespace Inscriptions { public Lang[] TargetLang () { return { new Lang ("system",_("System language")), - new Lang ("AR",_("Arabic")), new Lang ("BG",_("Bulgarian")), new Lang ("CS",_("Czech")), new Lang ("DA",_("Danish")), new Lang ("DE",_("German")), new Lang ("EL",_("Greek")), - new Lang ("EN",_("English (GB)")), - new Lang ("EN",_("English (US)")), + new Lang ("EN-GB",_("English (GB)")), + new Lang ("EN-US",_("English (US)")), + new Lang ("EO",_("Spanish (All)")), new Lang ("ES",_("Spanish (All)")), new Lang ("ES-419",_("Spanish (Latin American)")), new Lang ("ET",_("Estonian")), diff --git a/src/Enums/HighlightColor.vala b/src/Enums/HighlightColor.vala index ccfc04a..1e1d6ca 100644 --- a/src/Enums/HighlightColor.vala +++ b/src/Enums/HighlightColor.vala @@ -3,6 +3,9 @@ * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) */ +// I like aligning the equals +//vala-lint=skip-file + /** * Used in the custom TextView, alternates background color between sentences. */ diff --git a/src/Enums/StatusCode.vala b/src/Enums/StatusCode.vala index 0a2d4c5..c02f968 100644 --- a/src/Enums/StatusCode.vala +++ b/src/Enums/StatusCode.vala @@ -3,15 +3,139 @@ * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) */ +// I like aligning the equals +//vala-lint=skip-file + /** * libsoup.Status does not have all error codes, so we extend handling in ErrorView/ErrorBonuxBox with these custom ones. * Custom codes we can use safely for at least 0 to 10. * Specific return codes to DeepL, we use their Int representation. */ public enum Inscriptions.StatusCode { - NO_KEY = 0, - NO_INTERNET = 1, - QUOTA = 456, - TOO_MANY_REQUESTS = 429, - SSL_HANDSHAKE_ERROR = 525; -} \ No newline at end of file + + // Custom + NO_KEY = 0, + NO_INTERNET = 1, + EDIT_KEY = 2, + UNKNOWN = 3, + + // 200 + OK = Soup.Status.OK, + + // 400 + BAD_REQUEST = Soup.Status.BAD_REQUEST, + FORBIDDEN = Soup.Status.FORBIDDEN, + REQUEST_TIMEOUT = Soup.Status.REQUEST_TIMEOUT, + TOO_MANY_REQUESTS = 429, + QUOTA = 456, + + // 500 + INTERNAL_SERVER_ERROR = Soup.Status.INTERNAL_SERVER_ERROR, + GATEWAY_TIMEOUT = Soup.Status.GATEWAY_TIMEOUT, + SSL_HANDSHAKE_ERROR = 525 + ; + + /** + * Utility function to make use of the various error codes, and return comprehensive explanations. + * It is here better to take the status than let the backend tell us, so we conserve the code for unknown errors. + * Yes, some refers to Soup.Status making the declaration above redundant, but i feel it may be cleaner to keep them in case we need a more native approach. + */ + public static void status_to_details (uint status, + out string explanation_title, out string explanation_text, out string icon_name, out bool report_link) { + + // Set some defaults + icon_name = "dialog-error"; + report_link = false; + + switch (status) { + //Custom status codes feel super evil + //TRANSLATORS: The following texts show up respectively, as a title, and error message, when translating has gone wrong. This needs to be as little technical as possible + case StatusCode.NO_KEY: + explanation_title = _("Hello, World!"); + explanation_text = _("You need a DeepL API key to translate text\nIt can be either DeepL Free or Pro"); + icon_name = "dialog-password"; + return; + + case StatusCode.NO_INTERNET: + explanation_title = _("No Internet"); + icon_name = "network-offline-symbolic"; + + if (Environment.get_variable ("XDG_CURRENT_DESKTOP") == "Pantheon") { + ///TRANSLATORS: This is twice the same text, but the first one has links for elementary OS + explanation_text = _("Please verify you are connected to the internet, and that this app has permission to access it").printf (Granite.SettingsUri.NETWORK, Granite.SettingsUri.PERMISSIONS); + } else { + explanation_text = _("Please verify you are connected to the internet, and that this app has permission to access it"); + } + return; + + case StatusCode.EDIT_KEY: + explanation_title = _("Edit API Key"); + explanation_text = _("Yo"); + icon_name = "dialog-password"; + return; + + + case Soup.Status.OK: + explanation_title = _("Everything works great :)"); + explanation_text = _("If you see this and are not me, then it means i forgor to disable this error"); + icon_name = "process-completed"; + report_link = true; + return; + + case Soup.Status.BAD_REQUEST: + explanation_title = _("Bad request"); + explanation_text = _("The app sent a wrong translation request to DeepL\nPlease report this to the app's developer with as much details as you can"); + icon_name = "dialog-warning"; + return; + + case Soup.Status.FORBIDDEN: + explanation_title = _("Forbidden"); + explanation_text = _("Your API key is invalid. Make sure it is the correct one!"); + icon_name = "dialog-error"; + return; + + case StatusCode.TOO_MANY_REQUESTS: + explanation_title = _("Too many requests"); + explanation_text = _("Please wait before retrying. This error should not be possible to happen for this app..."); + icon_name = "dialog-warning"; + return; + + case StatusCode.QUOTA: + explanation_title = _("Your monthly quota has been exceeded"); + explanation_text = _("If you are a Pro API user, this corresponds to your Cost Control limit"); + icon_name = "dialog-warning"; + return; + + case Soup.Status.INTERNAL_SERVER_ERROR: + explanation_title = _("Internal server error"); + explanation_text = _("Retry in a minute? If you see this several times, check online if there is a DeepL service interruption"); + icon_name = "dialog-information"; + return; + + case StatusCode.SSL_HANDSHAKE_ERROR: + explanation_title = _("SSL Handshake error"); + explanation_text = _("This is an issue DeepL is aware of and this app can do nothing about...\nIf you have the know-show, going through a simple authenticated proxy may work"); + icon_name = "network-error"; + return; + + case Soup.Status.REQUEST_TIMEOUT: + explanation_title = _("Request timeout"); + explanation_text = _("No answer has been received. Either DeepL or your connection are having issues"); + icon_name = "network-error"; + return; + + case Soup.Status.GATEWAY_TIMEOUT: + explanation_title = _("Gateway timeout"); + explanation_text = _("No answer has been received. Either DeepL or your connection are having issues"); + icon_name = "network-error"; + return; + + default: + explanation_title = _("Unknown error"); + explanation_text = _("Status code %s, please report this to this app's developer").printf(status.to_string ()); + icon_name = "dialog-question"; + report_link = true; + return; + } + } +} diff --git a/src/Enums/ZoomLevel.vala b/src/Enums/ZoomLevel.vala new file mode 100644 index 0000000..b277a6b --- /dev/null +++ b/src/Enums/ZoomLevel.vala @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017-2024 Lains + * 2025 Contributions from the ellie_Commons community (github.com/ellie-commons/) + * 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + +/*************************************************/ +/** +* A register of all possible zoom values we have +*/ +public enum Inscriptions.ZoomLevel { + ANTSIZED, + MUCHSMALLER, + SMALLER, + SMALL, + NORMAL, + BIG, + BIGGER, + MUCHBIGGER, + MUCHMUCHBIGGER, + HUGE, + SUPERHUGE, + MEGAHUGE, + ULTRAHUGE, + MASSIVE, + URPARENT; + + /*************************************************/ + /** + * Returns an Int representation we can use to display and store the value + */ + public int to_int () { + switch (this) { + case ANTSIZED: return 20; + case MUCHSMALLER: return 40; + case SMALLER: return 60; + case SMALL: return 80; + case NORMAL: return 100; + case BIG: return 120; + case BIGGER: return 140; + case MUCHBIGGER: return 160; + case MUCHMUCHBIGGER: return 180; + case HUGE: return 200; + case SUPERHUGE: return 220; + case MEGAHUGE: return 240; + case ULTRAHUGE: return 260; + case MASSIVE: return 280; + case URPARENT: return 300; + default: return 100; + } + } + + /*************************************************/ + /** + * CSS name is s + size. CSS classes cannot start name with number + */ + public string to_css_class () { + return "s" + this.to_int ().to_string (); + } + + /*************************************************/ + /** + * We cannot save Enums in JSON, so this recovers the enum from stored int + */ + public static ZoomLevel from_int (int wtf_is_this) { + switch (wtf_is_this) { + case 20: return ANTSIZED; + case 40: return MUCHSMALLER; + case 60: return SMALLER; + case 80: return SMALL; + case 100: return NORMAL; + case 120: return BIG; + case 140: return BIGGER; + case 160: return MUCHBIGGER; + case 180: return MUCHMUCHBIGGER; + case 200: return HUGE; + case 220: return SUPERHUGE; + case 240: return MEGAHUGE; + case 260: return ULTRAHUGE; + case 280: return MASSIVE; + case 300: return URPARENT; + default: return NORMAL; + } + } +} diff --git a/src/Enums/ZoomType.vala b/src/Enums/ZoomType.vala new file mode 100644 index 0000000..bbdc38f --- /dev/null +++ b/src/Enums/ZoomType.vala @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017-2024 Lains + * 2025 Contributions from the ellie_Commons community (github.com/ellie-commons/) + * 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + + /*************************************************/ +/** +* Used in a signal to tell windows in which way to change zoom +*/ + public enum Inscriptions.ZoomType { + ZOOM_OUT, + DEFAULT_ZOOM, + ZOOM_IN, + NONE; + + public static ZoomType from_delta (double delta) { + + if (delta == 0) {return NONE;} + + if (delta > 0) + { + return ZOOM_OUT; + + } else { + return ZOOM_IN; + } + } +} \ No newline at end of file diff --git a/src/Objects/DDModel.vala b/src/Objects/DDModel.vala index 3024767..2c91e68 100644 --- a/src/Objects/DDModel.vala +++ b/src/Objects/DDModel.vala @@ -6,29 +6,78 @@ /** * DDModel, to manage lists of Lang objects (Languages), notably for the dropdown of Pane - * Thank you stronnag! + * In human, this is to have a custom dropdown for language selection */ public class Inscriptions.DDModel : Object { public GLib.ListStore model {get; set;} - public Gtk.SignalListItemFactory factory {get; set;} + public Gtk.SignalListItemFactory factory_header {get; set;} + public Gtk.SignalListItemFactory factory_list {get; set;} - public DDModel() { + // Signal emitted by factory_header.bind when a language is selected, which factory_list.bind listens to + public signal void selection_changed (string language_code_selected); + + public DDModel () { + // The Langs will populate this thing model = new GLib.ListStore(typeof(Lang)); - factory = new Gtk.SignalListItemFactory(); - factory.setup.connect ((f,o) => { - Gtk.ListItem list_item = (Gtk.ListItem)o; - var label=new Gtk.Label(""); - list_item.set_child(label); - }); - factory.bind.connect ((f,o) => { - Gtk.ListItem list_item = (Gtk.ListItem)o; - var language = list_item.get_item () as Lang; - var label = list_item.get_child() as Gtk.Label; - label.set_text(language.name); - }); - } - - public void model_append(Lang l) { + + factory_header = new Gtk.SignalListItemFactory(); + factory_list = new Gtk.SignalListItemFactory(); + + // This one does simple labels for each language, to be shown by the dropdown + factory_header.setup.connect (on_factory_header_setup); + factory_header.bind.connect (on_factory_header_bind); + + // This one generates a custom widget for each element in the popup list + factory_list.setup.connect (on_factory_list_setup); + factory_list.bind.connect (on_factory_list_bind); + } + + // ---------------------------------------- + /* DROPDOWN VISIBLE LABEL */ + private void on_factory_header_setup (Gtk.SignalListItemFactory f, Object o) { + var list_item = (Gtk.ListItem)o; + list_item.child = new Gtk.Label (""); + list_item.focusable = true; + } + + private void on_factory_header_bind (Gtk.SignalListItemFactory f, Object o) { + var list_item = (Gtk.ListItem)o; + var item_language = list_item.get_item () as Lang; + var item = list_item.get_child () as Gtk.Label; + item.label = item_language.name; + + // Tell everyone language changed + selection_changed (item_language.code); + //print ("switched to: %s %s\n".printf (item_language.name, item_language.code)); + } + + // ---------------------------------------- + /* DROPDOWN POPUP LIST */ + private void on_factory_list_setup (Gtk.SignalListItemFactory f, Object o) { + var list_item = (Gtk.ListItem)o; + var list_item_child = new Inscriptions.LanguageItem ("", ""); + + list_item.child = list_item_child; + list_item.focusable = true; + } + + private void on_factory_list_bind (Gtk.SignalListItemFactory f, Object o) { + var list_item = (Gtk.ListItem)o; + var item_language = list_item.get_item () as Lang; + + var list_item_child = list_item.get_child() as Inscriptions.LanguageItem; + list_item_child.language_label = item_language.name; + list_item_child.language_code = item_language.code; + + // Listen to language change, let every item sort its shit + selection_changed.connect (list_item_child.on_position_changed); + //print ("binding: %s\n".printf (item_language.name)); + } + + + // ---------------------------------------- + /* LIST MANAGEMENT */ + public void model_append (Lang l) { model.append (l); } diff --git a/src/Objects/Lang.vala b/src/Objects/Lang.vala index 36bcbfa..c22c68c 100644 --- a/src/Objects/Lang.vala +++ b/src/Objects/Lang.vala @@ -12,15 +12,16 @@ public class Inscriptions.Lang : Object { public string code {get; construct;} public string name {get; construct;} public string both {get; construct;} + public bool enabled {get; set; default = false;} public Lang (string code, string name) { - Object( code: code, + Object (code: code, name: name); } // "Both" serves to evaluate both name and code in a single expression construct { - both = name + "|" + code; + both = "%s|%s".printf(name, code); } public bool efunc(Lang a, Lang b) { diff --git a/src/Services/ZoomController.vala b/src/Services/ZoomController.vala new file mode 100644 index 0000000..2dd2fe7 --- /dev/null +++ b/src/Services/ZoomController.vala @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017-2024 Lains + * 2025 Contributions from the ellie_Commons community (github.com/ellie-commons/) + * 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + +/*************************************************/ +/** +* Responsible to apply zoom appropriately to a window. +* Mainly, this abstracts zoom into an int and swap CSS classes +* As a treat it includes also the plumbing for ctrl+scroll zooming +*/ +public class Inscriptions.ZoomController : Object { + + private static bool is_control_key_pressed = false; + private weak Gtk.Widget widget; + + const int ZOOM_MAX = 300; + const int DEFAULT_ZOOM = 100; + const int ZOOM_MIN = 20; + + + // Avoid setting this unless it is to restore a specific value, do_set_zoom does not check input + private int _old_zoom = DEFAULT_ZOOM; + public int zoom { + get {return _old_zoom;} + set {do_set_zoom (value);} + } + + public ZoomController (Gtk.Widget widget) { + this.widget = widget; + do_set_zoom (DEFAULT_ZOOM); + } + + /** + * Handler. Wraps a zoom enum into the correct function- + */ + public void zoom_changed (ZoomType zoomtype) { + print ("Zoom changed!"); + switch (zoomtype) { + case ZoomType.ZOOM_IN: zoom_in (); return; // vala-lint=double-spaces + case ZoomType.DEFAULT_ZOOM: zoom_default (); return; // vala-lint=double-spaces + case ZoomType.ZOOM_OUT: zoom_out (); return; // vala-lint=double-spaces + default: return; // vala-lint=double-spaces + } + } + + /** + * Wrapper to check an increase doesnt go above limit + */ + public void zoom_in () { + if ((_old_zoom + 20) <= ZOOM_MAX) { + zoom = _old_zoom + 20; + } else { + Gdk.Display.get_default ().beep (); + } + } + + public void zoom_default () { + if (_old_zoom != DEFAULT_ZOOM ) { + zoom = DEFAULT_ZOOM; + } else { + Gdk.Display.get_default ().beep (); + } + } + + /** + * Wrapper to check an increase doesnt go below limit + */ + public void zoom_out () { + if ((_old_zoom - 20) >= ZOOM_MIN) { + zoom = _old_zoom - 20; + } else { + Gdk.Display.get_default ().beep (); + } + } + + /** + * Switch zoom classes, then reflect in the UI and tell the application + */ + private void do_set_zoom (int new_zoom) { + print ("Setting zoom: " + zoom.to_string ()); + + // Switches the classes that control font size + widget.remove_css_class (ZoomLevel.from_int ( _old_zoom).to_css_class ()); + _old_zoom = new_zoom; + widget.add_css_class (ZoomLevel.from_int ( new_zoom).to_css_class ()); + } + + public bool on_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { + if (keyval == Gdk.Key.Control_L || keyval == Gdk.Key.Control_R) { + print ("Press!"); + is_control_key_pressed = true; + } + + return Gdk.EVENT_PROPAGATE; + } + + public void on_key_release_event (uint keyval, uint keycode, Gdk.ModifierType state) { + if (keyval == Gdk.Key.Control_L || keyval == Gdk.Key.Control_R) { + print ("Release!"); + is_control_key_pressed = false; + } + } + + public bool on_scroll (double dx, double dy) { + //print ("Scroll + Ctrl!"); + + if (!is_control_key_pressed) { + return Gdk.EVENT_PROPAGATE; + } + + zoom_changed (ZoomType.from_delta (dy)); + //print ("Go! Zoooommmmm"); + + return Gdk.EVENT_PROPAGATE; + } +} diff --git a/src/Views/ErrorView.vala b/src/Views/ErrorView.vala index 302ca13..1e734da 100644 --- a/src/Views/ErrorView.vala +++ b/src/Views/ErrorView.vala @@ -11,10 +11,12 @@ public class Inscriptions.ErrorView : Granite.Bin { const uint WAIT_BEFORE_MAIN = 1500; //In milliseconds + ErrorBonusBox bonusbox; + public uint status { get; construct; } public string message { get; construct; } - - string icon_name = "dialog-error"; + + string icon_name; string explanation_title; string explanation_text; bool report_link; @@ -38,7 +40,8 @@ public class Inscriptions.ErrorView : Granite.Bin { margin_bottom = MARGIN_MENU_BIGGER, }; - status_to_message (status); + Inscriptions.StatusCode.status_to_details (status, + out explanation_title, out explanation_text, out icon_name, out report_link); var title = new Granite.Placeholder (explanation_title) { description = explanation_text, @@ -49,7 +52,8 @@ public class Inscriptions.ErrorView : Granite.Bin { box.append (title); // WEIRD: We get errors about TRUE being out of range for a gboolean and the value defaulting if we leave a default - box.append (new ErrorBonusBox (status, report_link)); + bonusbox = new ErrorBonusBox (status, report_link); + box.append (bonusbox); var retry_button = new Inscriptions.RetryButton () { halign = Gtk.Align.END @@ -80,7 +84,7 @@ public class Inscriptions.ErrorView : Granite.Bin { margin_top = 12 }; - if (status != StatusCode.NO_KEY) { + if (status != StatusCode.NO_KEY || status != StatusCode.EDIT_KEY) { box.append (expander); } @@ -89,96 +93,10 @@ public class Inscriptions.ErrorView : Granite.Bin { }; child = handle; - - } - - private void status_to_message (uint status) { - switch (status) { - //Custom status codes feel super evil - //TRANSLATORS: The following texts show up respectively, as a title, and error message, when translating has gone wrong. This needs to be as little technical as possible - case StatusCode.NO_KEY: - explanation_title = _("Hello, World!"); - explanation_text = _("You need a DeepL API key to translate text\nIt can be either DeepL Free or Pro"); - icon_name = "dialog-password"; - return; - - case StatusCode.NO_INTERNET: - explanation_title = _("No Internet"); - icon_name = "network-offline-symbolic"; - - if (Environment.get_variable ("XDG_CURRENT_DESKTOP") == "Pantheon") { - ///TRANSLATORS: This is twice the same text, but the first one has links for elementary OS - explanation_text = _("Please verify you are connected to the internet, and that this app has permission to access it").printf (Granite.SettingsUri.NETWORK, Granite.SettingsUri.PERMISSIONS); - } else { - explanation_text = _("Please verify you are connected to the internet, and that this app has permission to access it"); - } - - return; - - case Soup.Status.OK: - explanation_title = _("Everything works great :)"); - explanation_text = _("If you see this and are not me, then it means i forgor to disable this error"); - icon_name = "process-completed"; - return; - - case Soup.Status.BAD_REQUEST: - explanation_title = _("Bad request"); - explanation_text = _("The app sent a wrong translation request to DeepL\nPlease report this to the app's developer with as much details as you can"); - icon_name = "dialog-warning"; - return; - - case Soup.Status.FORBIDDEN: - explanation_title = _("Forbidden"); - explanation_text = _("Your API key is invalid. Make sure it is the correct one!"); - icon_name = "dialog-error"; - return; - - case StatusCode.TOO_MANY_REQUESTS: - explanation_title = _("Too many requests"); - explanation_text = _("Please wait before retrying. This error should not be possible to happen for this app..."); - icon_name = "dialog-warning"; - return; - - case StatusCode.QUOTA: - explanation_title = _("Your monthly quota has been exceeded"); - explanation_text = _("If you are a Pro API user, this corresponds to your Cost Control limit"); - icon_name = "dialog-warning"; - return; - - case Soup.Status.INTERNAL_SERVER_ERROR: - explanation_title = _("Internal server error"); - explanation_text = _("Retry in a minute? If you see this several times, check online if there is a DeepL service interruption"); - icon_name = "dialog-information"; - return; - - case StatusCode.SSL_HANDSHAKE_ERROR: - explanation_title = _("SSL Handshake error"); - explanation_text = _("This is an issue DeepL is aware of and this app can do nothing about...\nIf you have the know-show, going through a simple authenticated proxy may work"); - icon_name = "network-error"; - return; - - case Soup.Status.REQUEST_TIMEOUT: - explanation_title = _("Request timeout"); - explanation_text = _("No answer has been received. Either DeepL or your connection are having issues"); - icon_name = "network-error"; - return; - - case Soup.Status.GATEWAY_TIMEOUT: - explanation_title = _("Gateway timeout"); - explanation_text = _("No answer has been received. Either DeepL or your connection are having issues"); - icon_name = "network-error"; - return; - - default: - explanation_title = _("Unknown error"); - explanation_text = _("Status code %s, please report this to this app's developer").printf(status.to_string ()); - icon_name = "dialog-question"; - report_link = true; - return; - } } private void on_validated () { + bonusbox.usage_revealer.reveal_child = true; Timeout.add_once (WAIT_BEFORE_MAIN, () => { return_to_main (); }); diff --git a/src/Views/TranslationView.vala b/src/Views/TranslationView.vala index a2a7084..13f5d7a 100644 --- a/src/Views/TranslationView.vala +++ b/src/Views/TranslationView.vala @@ -216,7 +216,7 @@ public class Inscriptions.TranslationView : Gtk.Box { source_pane.clear (); target_pane.clear (); target_pane.show_placeholder (); - source_pane.message (_("Cleared")); + source_pane.message (_("Cleared"), true); } public void action_load_text () { diff --git a/src/Widgets/ErrorBonusBox.vala b/src/Widgets/ErrorBonusBox.vala index 765bc62..5f6ca0c 100644 --- a/src/Widgets/ErrorBonusBox.vala +++ b/src/Widgets/ErrorBonusBox.vala @@ -14,6 +14,8 @@ public class Inscriptions.ErrorBonusBox : Gtk.Box { public uint status { get; construct; } public bool if_report { get; construct; } + public Gtk.Revealer usage_revealer; + public ErrorBonusBox (uint status, bool if_report) { Object (status: status, if_report: if_report); @@ -26,7 +28,9 @@ public class Inscriptions.ErrorBonusBox : Gtk.Box { margin_bottom = MARGIN_MENU_STANDARD; // In the event the API is the issue, ask user - if (status == Soup.Status.FORBIDDEN || status == StatusCode.NO_KEY) { + StatusCode[] api_edit_list = {StatusCode.NO_KEY, StatusCode.FORBIDDEN, StatusCode.EDIT_KEY}; + + if (status in api_edit_list) { var api_entry = new Inscriptions.ApiEntry (); @@ -35,7 +39,16 @@ public class Inscriptions.ErrorBonusBox : Gtk.Box { halign = Gtk.Align.START }; - if (status == StatusCode.NO_KEY) { + var api_level = new Inscriptions.ApiLevel (); + usage_revealer = new Gtk.Revealer () { + transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN, + transition_duration = 500, + child = api_level, + reveal_child = (status != StatusCode.NO_KEY) + }; + + + if (status == StatusCode.NO_KEY || status == StatusCode.FORBIDDEN) { var explanation = new Gtk.Label (_("An API Key is like a password given by DeepL\nIt allows you to access services from applications such as this one\nIt looks like this: fr5617a-4875-4763-9119-564tjdvg89:fx")) { wrap_mode = Pango.WrapMode.WORD_CHAR, halign = Gtk.Align.START @@ -47,6 +60,11 @@ public class Inscriptions.ErrorBonusBox : Gtk.Box { append (api_entry); append (link); + //append (usage_revealer); + + + + }; diff --git a/src/Widgets/HeaderBar.vala b/src/Widgets/HeaderBar.vala index 7159fc0..d995917 100644 --- a/src/Widgets/HeaderBar.vala +++ b/src/Widgets/HeaderBar.vala @@ -65,7 +65,10 @@ public class Inscriptions.HeaderBar : Granite.Bin { app.set_accels_for_action (ACTION_PREFIX + ACTION_MENU, {"