diff --git a/.gitignore b/.gitignore index 05826c6..58b9868 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,5 @@ _ignore build build.sh build-po.sh -install.sh uninstall.sh *~ diff --git a/README.md b/README.md index cd821f6..a504fb7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ -![Tootle](https://user-images.githubusercontent.com/37731582/39933812-45d8149a-5544-11e8-9bf4-6d78b1fdb29c.png) -Simple [Mastodon](https://github.com/tootsuite/mastodon) client designed for elementary OS. +![Tootle](https://raw.githubusercontent.com/bleakgrey/tootle/master/data/icons/color.svg) +Simple [Mastodon](https://github.com/tootsuite/mastodon) client for Linux -![Tootle Screenshot](https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot.png) +![Screenshot](https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot.png) -## Building and Installation +## Installation +This project is undergoing a major rewrite and will be published in the near future. + +To help the project, please build it manually and help test it. -[![Get it on AppCenter](https://appcenter.elementary.io/badge.svg)](https://appcenter.elementary.io/com.github.bleakgrey.tootle) Download on Flathub -First of all you'll need some dependencies to build and run the app: +## Building +To build the app, make sure you have these dependencies: * meson * valac * libgtk-3-dev @@ -16,26 +19,19 @@ First of all you'll need some dependencies to build and run the app: * libgranite-dev * libjson-glib-dev -Then run these commands to build and install it: +Then run 'install.sh' in the project directory to install the app. - meson build --prefix=/usr - cd build - sudo ninja install - com.github.bleakgrey.tootle - ## Contributing -If you feel like contributing, you're always welcome to help the project in many ways: -* Reporting any issues -* Suggesting ideas and functionality +You're always welcome to help the project in many ways: +* Donating with [LiberaPay](https://liberapay.com/bleakgrey/) to keep the developer happy and motivated +* Reporting issues and bugs * Submitting pull requests -* Donating with [LiberaPay](https://liberapay.com/bleakgrey/) to help project development and keeping the developer happy Donate using Liberapay ## Credits -* Tootle Logo by [@CallMeFib3r](https://github.com/CallMeFib3r) -* Medel typeface by Ozan Karakoc +* Icon design by [Tobias Bernard](https://github.com/bertob) * French translation by [@Larnicone](https://github.com/Larnicone) * Polish translation by [@m4sk1n](https://github.com/m4sk1n) * German translation by [@koyuawsmbrtn](https://github.com/koyuawsmbrtn) diff --git a/data/app.css b/data/app.css index 34465bc..2c8dca7 100644 --- a/data/app.css +++ b/data/app.css @@ -1,35 +1,12 @@ -.titlebar.compact { - padding: 0 6px; +.avatar { + border-radius: 4px; } -.mode .toggle{ - border-radius:0px; - border-top:none; - border-bottom:none; - padding:10px; - margin:0px; +.attachment { + border-radius: 4px; + background: rgba (150, 150, 150, 0.2); } -.button_avatar{ - padding:0; - border:0; - box-shadow:none; - background:none; -} - -.toot-text, .toot-text text{ - background-color: transparent; -} - -.header{ - background-size: cover; - background-position: 50%; - opacity: 0.15; -} - -.relationship { - background: rgba (0,0,0,.5); - padding: 6px; - border-radius: 3px; - color: #fff; +.highlight { + background: @theme_base_color; } diff --git a/data/com.github.bleakgrey.tootle.appdata.xml.in b/data/com.github.bleakgrey.tootle.appdata.xml.in index 57c7088..822d1eb 100644 --- a/data/com.github.bleakgrey.tootle.appdata.xml.in +++ b/data/com.github.bleakgrey.tootle.appdata.xml.in @@ -6,7 +6,7 @@ GPL-3.0+ Tootle Lightning fast client for Mastodon - +

Tootle is a client for the world’s largest free, open-source, decentralized microblogging network with real-time notifications and support for multiple accounts. @@ -18,16 +18,16 @@ Anyone can run a Mastodon server. Each server hosts individual user accounts, the content they produce, and the content to which they are subscribed. Every user can follow each other and share their posts regardless of their server.

- + com.github.bleakgrey.tootle - + bleak_grey https://github.com/bleakgrey https://github.com/bleakgrey/tootle/issues https://liberapay.com/bleakgrey/donate - + none none @@ -57,20 +57,11 @@ none none - + https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot.png - - https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot2.png - - - https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot3.png - - - https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot4.png - @@ -111,7 +102,7 @@ - + #F5F8FF #413F58 diff --git a/data/com.github.bleakgrey.tootle.gresource.xml b/data/com.github.bleakgrey.tootle.gresource.xml deleted file mode 100644 index 71d04bb..0000000 --- a/data/com.github.bleakgrey.tootle.gresource.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - app.css - light.css - dark.css - logo128.png - empty_state.png - - diff --git a/data/com.github.bleakgrey.tootle.gschema.xml b/data/com.github.bleakgrey.tootle.gschema.xml index 2f308b7..647680c 100644 --- a/data/com.github.bleakgrey.tootle.gschema.xml +++ b/data/com.github.bleakgrey.tootle.gschema.xml @@ -16,16 +16,6 @@ Always monitor new notifications - - false - Cache images to reduce network load - - - - 64 - Cache size - Sets the maximum size of cached content - true Real-time timelines diff --git a/data/dark.css b/data/dark.css deleted file mode 100644 index eb3a0aa..0000000 --- a/data/dark.css +++ /dev/null @@ -1,14 +0,0 @@ -@define-color colorAccent #c92e34; -@define-color colorPrimary #35393c; - -.header-counters{ - background: rgba(0,0,0,.2); -} - -.attachment{ - background: rgba (255,255,255,.15); -} - -.card{ - background: rgba (255,255,255,.15); -} diff --git a/data/empty_state.png b/data/empty_state.png deleted file mode 100644 index 4a0cfcc..0000000 Binary files a/data/empty_state.png and /dev/null differ diff --git a/data/gresource.xml b/data/gresource.xml new file mode 100644 index 0000000..52bf864 --- /dev/null +++ b/data/gresource.xml @@ -0,0 +1,14 @@ + + + + app.css + ui/views/new_account.ui + ui/views/base.ui + ui/views/profile_header.ui + ui/widgets/status.ui + ui/widgets/accounts_button.ui + ui/widgets/accounts_button_item.ui + ui/dialogs/compose.ui + ui/dialogs/main.ui + + diff --git a/data/icons/128/com.github.bleakgrey.tootle.svg b/data/icons/128/com.github.bleakgrey.tootle.svg deleted file mode 100644 index 9d564ab..0000000 --- a/data/icons/128/com.github.bleakgrey.tootle.svg +++ /dev/null @@ -1,188 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/data/icons/16/com.github.bleakgrey.tootle.svg b/data/icons/16/com.github.bleakgrey.tootle.svg deleted file mode 100644 index a82f8ce..0000000 --- a/data/icons/16/com.github.bleakgrey.tootle.svg +++ /dev/null @@ -1,125 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/data/icons/24/com.github.bleakgrey.tootle.svg b/data/icons/24/com.github.bleakgrey.tootle.svg deleted file mode 100644 index ab3aeef..0000000 --- a/data/icons/24/com.github.bleakgrey.tootle.svg +++ /dev/null @@ -1,189 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/data/icons/32/com.github.bleakgrey.tootle.svg b/data/icons/32/com.github.bleakgrey.tootle.svg deleted file mode 100644 index 5dc33c6..0000000 --- a/data/icons/32/com.github.bleakgrey.tootle.svg +++ /dev/null @@ -1,185 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/data/icons/48/com.github.bleakgrey.tootle.svg b/data/icons/48/com.github.bleakgrey.tootle.svg deleted file mode 100644 index 36c40ee..0000000 --- a/data/icons/48/com.github.bleakgrey.tootle.svg +++ /dev/null @@ -1,184 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/data/icons/64/com.github.bleakgrey.tootle.svg b/data/icons/64/com.github.bleakgrey.tootle.svg deleted file mode 100644 index f371cf4..0000000 --- a/data/icons/64/com.github.bleakgrey.tootle.svg +++ /dev/null @@ -1,184 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/data/icons/LICENSE b/data/icons/LICENSE deleted file mode 100644 index 1a07481..0000000 --- a/data/icons/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 CallMeFib3r - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/data/icons/color-nightly.svg b/data/icons/color-nightly.svg new file mode 100644 index 0000000..0a6efd0 --- /dev/null +++ b/data/icons/color-nightly.svg @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/color.svg b/data/icons/color.svg new file mode 100644 index 0000000..f254da9 --- /dev/null +++ b/data/icons/color.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/symbolic.svg b/data/icons/symbolic.svg new file mode 100644 index 0000000..741ad91 --- /dev/null +++ b/data/icons/symbolic.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/light.css b/data/light.css deleted file mode 100644 index 65d4409..0000000 --- a/data/light.css +++ /dev/null @@ -1,14 +0,0 @@ -@define-color colorAccent #9aa7c8; -@define-color colorPrimary #9aa7c8; - -.header-counters{ - background: rgba(255,255,255,.4); -} - -.attachment{ - background: rgba (255,255,255,.8); -} - -.card{ - background: #fff; -} diff --git a/data/logo128.png b/data/logo128.png deleted file mode 100644 index ffdeb24..0000000 Binary files a/data/logo128.png and /dev/null differ diff --git a/data/meson.build b/data/meson.build index f0c4ef7..ac5f6f7 100644 --- a/data/meson.build +++ b/data/meson.build @@ -1,15 +1,17 @@ -icon_sizes = ['16', '24', '32', '48', '64', '128'] +icons_dir = join_paths(get_option('datadir'), 'icons', 'hicolor') +scalable_dir = join_paths(icons_dir, 'scalable', 'apps') +symbolic_dir = join_paths(icons_dir, 'symbolic', 'apps') -foreach i : icon_sizes - install_data( - join_paths('icons', i, meson.project_name() + '.svg'), - install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', i + 'x' + i, 'apps') - ) - install_data( - join_paths('icons', i, meson.project_name() + '.svg'), - install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', i + 'x' + i + '@2', 'apps') - ) -endforeach +install_data( + join_paths('icons', 'color.svg'), + install_dir: scalable_dir, + rename: meson.project_name() + '.svg' +) +install_data( + join_paths('icons', 'symbolic.svg'), + install_dir: symbolic_dir, + rename: meson.project_name() + '-symbolic.svg' +) install_data( meson.project_name() + '.gschema.xml', diff --git a/data/screenshot.png b/data/screenshot.png index 9ea9f7c..bf28089 100644 Binary files a/data/screenshot.png and b/data/screenshot.png differ diff --git a/data/screenshot2.png b/data/screenshot2.png deleted file mode 100644 index 316704c..0000000 Binary files a/data/screenshot2.png and /dev/null differ diff --git a/data/screenshot3.png b/data/screenshot3.png deleted file mode 100644 index 14330a1..0000000 Binary files a/data/screenshot3.png and /dev/null differ diff --git a/data/screenshot4.png b/data/screenshot4.png deleted file mode 100644 index b108218..0000000 Binary files a/data/screenshot4.png and /dev/null differ diff --git a/data/ui/dialogs/compose.ui b/data/ui/dialogs/compose.ui new file mode 100644 index 0000000..d99c069 --- /dev/null +++ b/data/ui/dialogs/compose.ui @@ -0,0 +1,255 @@ + + + + + + True + False + dialog-warning-symbolic + + + True + False + mail-attachment-symbolic + + + True + False + face-smile-symbolic + + + diff --git a/data/ui/dialogs/main.ui b/data/ui/dialogs/main.ui new file mode 100644 index 0000000..9a2b527 --- /dev/null +++ b/data/ui/dialogs/main.ui @@ -0,0 +1,82 @@ + + + + + + diff --git a/data/ui/views/base.ui b/data/ui/views/base.ui new file mode 100644 index 0000000..e187792 --- /dev/null +++ b/data/ui/views/base.ui @@ -0,0 +1,155 @@ + + + + + + diff --git a/data/ui/views/new_account.ui b/data/ui/views/new_account.ui new file mode 100644 index 0000000..c2c08cc --- /dev/null +++ b/data/ui/views/new_account.ui @@ -0,0 +1,224 @@ + + + + + + 350 + 400 + True + False + center + center + 12 + 12 + vertical + 12 + + + True + False + 12 + 12 + True + + + True + True + True + True + + + True + False + 8 + 8 + 8 + 8 + go-next-symbolic + + + + + + True + False + end + 1 + + + + + 0 + 2 + + + + + True + False + True + True + slide-left-right + + + True + False + 12 + 12 + True + True + 6 + 6 + + + True + False + True + Which Instance? + + center + True + + + + 0 + 0 + 2 + + + + + True + False + end + start + <a href="https://joinmastodon.org/">What's an instance?</a> + True + right + False + + + 1 + 2 + + + + + True + True + True + True + False + instance.domain + url + + + 0 + 1 + 2 + + + + + + + + + + True + False + 12 + 12 + True + True + 6 + 6 + + + True + False + True + Grant Account Access + + center + True + + + + 0 + 0 + 2 + + + + + True + True + True + edit-paste-symbolic + Paste + Paste your authorization code here + url + + + 0 + 1 + 2 + + + + + True + True + end + start + <a href="">Try another instance?</a> + True + right + False + + + 1 + 2 + + + + + + + + page1 + page1 + 1 + + + + + 0 + 1 + + + + + True + False + True + True + 128 + com.github.bleakgrey.tootle + 6 + + + 0 + 0 + + + + diff --git a/data/ui/views/profile_header.ui b/data/ui/views/profile_header.ui new file mode 100644 index 0000000..2b25001 --- /dev/null +++ b/data/ui/views/profile_header.ui @@ -0,0 +1,525 @@ + + + + + + False + + + True + False + 8 + 8 + 8 + 8 + vertical + 8 + True + + + Posts + True + True + False + True + True + + + False + True + 0 + + + + + Posts and Replies + True + True + False + True + True + filter_all + + + False + True + 1 + + + + + Media + True + True + False + True + True + filter_all + + + False + True + 2 + + + + + + + True + False + + + True + False + __glade_unnamed_3 + True + + + + + True + False + __glade_unnamed_8 + True + + + + + True + False + __glade_unnamed_9 + True + + + + + 400 + True + False + 8 + 8 + 8 + 8 + 8 + 8 + + + True + False + 0 + in + + + True + False + none + False + + + True + False + False + False + + + True + True + False + 8 + 8 + 8 + 8 + True + 25 + + + + + + + + + + + + 1 + 2 + + + + + True + False + 0 + in + + + True + False + none + False + + + True + False + False + False + + + True + False + 8 + 8 + 8 + 8 + 8 + + + 40 + True + False + True + 32 + + + True + True + False + True + True + + + True + False + 0 Posts + True + + + + + + True + True + 0 + + + + + True + True + False + True + posts_tab + + + True + False + 0 Follows + True + + + + + + True + True + 1 + + + + + True + True + False + True + posts_tab + + + True + False + 0 Followers + True + + + + + + True + True + 2 + + + + + + False + True + 0 + + + + + True + False + vertical + + + False + True + 1 + + + + + True + True + True + none + True + filter_popover + + + True + False + view-more-symbolic + + + + + + False + True + 2 + + + + + + + + + + + + + + 1 + 3 + + + + + True + False + 0 + in + + + True + False + none + False + + + 100 + 80 + True + False + False + False + + + True + False + 8 + 8 + 8 + 8 + 8 + + + True + 8 + 8 + 8 + 8 + 128 + + + False + True + 0 + + + + + True + False + center + 8 + 8 + vertical + 8 + + + True + True + + + False + True + 0 + + + + + True + True + + + False + True + 1 + + + + + True + True + 2 + + + + + + + + + True + False + False + False + + + True + False + + + + + + + True + False + False + False + + + True + False + 8 + 8 + 8 + 8 + 8 + + + True + False + False + 0 + + + True + True + 0 + + + + + True + False + False + + + Follow + True + True + True + + + False + True + 0 + + + + + True + True + True + True + options + False + + + + + + False + True + 1 + + + + + + False + True + 1 + + + + + + + + + + + 1 + 1 + + + + + + + + + + + + + + + + + + + diff --git a/data/ui/widgets/accounts_button.ui b/data/ui/widgets/accounts_button.ui new file mode 100644 index 0000000..41253dd --- /dev/null +++ b/data/ui/widgets/accounts_button.ui @@ -0,0 +1,240 @@ + + + + + + False + + + True + False + False + False + slide-left-right + True + + + True + False + 4 + vertical + 2 + + + True + True + True + + True + accounts + + + False + True + 0 + + + + + + + + True + False + + + False + True + 2 + + + + + True + True + True + Favorites + + + False + True + 3 + + + + + True + True + True + Conversations + + + False + True + 4 + + + + + True + True + Watchlist + + + False + True + 5 + + + + + True + False + + + False + True + 6 + + + + + True + True + True + Refresh + + + False + True + 7 + + + + + True + True + True + Search + + + False + True + 8 + + + + + True + True + True + Preferences + + + False + True + 9 + + + + + menu + page0 + + + + + 400 + True + False + 4 + vertical + 6 + + + True + True + True + Accounts + menu + True + True + + + False + True + 0 + + + + + True + True + True + in + 300 + True + + + True + False + + + True + False + + + + + + + False + True + 1 + + + + + accounts + page1 + 1 + + + + + + + diff --git a/data/ui/widgets/accounts_button_item.ui b/data/ui/widgets/accounts_button_item.ui new file mode 100644 index 0000000..3f87153 --- /dev/null +++ b/data/ui/widgets/accounts_button_item.ui @@ -0,0 +1,113 @@ + + + + + + diff --git a/data/ui/widgets/status.ui b/data/ui/widgets/status.ui new file mode 100644 index 0000000..4316590 --- /dev/null +++ b/data/ui/widgets/status.ui @@ -0,0 +1,293 @@ + + + + + + True + False + emblem-favorite-symbolic + + + True + False + mail-replied-symbolic + + + True + False + media-playlist-repeat-symbolic + + + diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..8db4601 --- /dev/null +++ b/install.sh @@ -0,0 +1,4 @@ +meson build --prefix=/usr +cd build +sudo ninja install +com.github.bleakgrey.tootle diff --git a/meson.build b/meson.build index 0776747..4664b6a 100644 --- a/meson.build +++ b/meson.build @@ -3,6 +3,9 @@ project('com.github.bleakgrey.tootle', 'vala', 'c') gnome = import('gnome') i18n = import('i18n') +#add_project_arguments(['--disable-warnings', '-g', '-X', '-rdynamic'], language: 'vala') +add_project_arguments(['-g', '-rdynamic', '-export-dynamic'], language: 'c') + add_global_arguments([ '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()) ], @@ -10,7 +13,7 @@ add_global_arguments([ ) asresources = gnome.compile_resources( - 'as-resources', 'data/' + meson.project_name() + '.gresource.xml', + 'as-resources', 'data/gresource.xml', source_dir: 'data', c_name: 'as' ) @@ -18,42 +21,48 @@ asresources = gnome.compile_resources( executable( meson.project_name(), asresources, + + 'src/Stacktrace.vala', #TODO: move into a separate lib + + 'src/Build.vala', 'src/Application.vala', 'src/Desktop.vala', 'src/Drawing.vala', 'src/Html.vala', - 'src/Settings.vala', - 'src/Accounts.vala', - 'src/ImageCache.vala', - 'src/Network.vala', - 'src/Watchlist.vala', - 'src/Notificator.vala', + 'src/Utils.vala', + 'src/Request.vala', 'src/InstanceAccount.vala', + 'src/Services/Streams.vala', + 'src/Services/Settings.vala', + 'src/Services/Accounts.vala', + 'src/Services/IAccountListener.vala', + 'src/Services/IStreamListener.vala', + 'src/Services/Cache.vala', + 'src/Services/Network.vala', 'src/API/Account.vala', 'src/API/Relationship.vala', 'src/API/Mention.vala', 'src/API/Tag.vala', 'src/API/Status.vala', - 'src/API/StatusVisibility.vala', + 'src/API/Visibility.vala', 'src/API/Notification.vala', 'src/API/NotificationType.vala', 'src/API/Attachment.vala', + 'src/Widgets/Avatar.vala', + 'src/Widgets/AccountsButton.vala', 'src/Widgets/AlignedLabel.vala', 'src/Widgets/RichLabel.vala', - 'src/Widgets/ImageToggleButton.vala', - 'src/Widgets/AccountsButton.vala', 'src/Widgets/Status.vala', - 'src/Widgets/Account.vala', 'src/Widgets/Notification.vala', - 'src/Widgets/ImageAttachment.vala', - 'src/Widgets/AttachmentGrid.vala', + 'src/Widgets/VisibilityPopover.vala', + 'src/Widgets/Attachment/Box.vala', + 'src/Widgets/Attachment/Item.vala', 'src/Dialogs/ISavedWindow.vala', 'src/Dialogs/MainWindow.vala', - 'src/Dialogs/NewAccount.vala', 'src/Dialogs/Compose.vala', 'src/Dialogs/Preferences.vala', - 'src/Dialogs/WatchlistEditor.vala', - 'src/Views/Abstract.vala', + 'src/Views/Base.vala', + 'src/Views/NewAccount.vala', 'src/Views/Timeline.vala', 'src/Views/Home.vala', 'src/Views/Local.vala', @@ -62,8 +71,6 @@ executable( 'src/Views/Direct.vala', 'src/Views/ExpandedStatus.vala', 'src/Views/Profile.vala', - 'src/Views/Followers.vala', - 'src/Views/Following.vala', 'src/Views/Favorites.vala', 'src/Views/Search.vala', 'src/Views/Hashtag.vala', @@ -73,9 +80,12 @@ executable( dependency('gee-0.8', version: '>=0.8.5'), dependency('granite', version: '>=5.2.0'), dependency('json-glib-1.0'), - dependency('libsoup-2.4') + dependency('libsoup-2.4'), + + meson.get_compiler('vala').find_library('linux', required: true), #Required by Stacktrace.vala ], - install: true + install: true, + link_args: '-export-dynamic' ) subdir('data') diff --git a/po/POTFILES b/po/POTFILES index e788f8c..6f34dd4 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,47 +1,53 @@ data/com.github.bleakgrey.tootle.desktop.in data/com.github.bleakgrey.tootle.appdata.xml.in -src/Accounts.vala +src/Build.vala src/Application.vala src/Desktop.vala +src/Drawing.vala src/Html.vala -src/ImageCache.vala +src/Utils.vala +src/Request.vala src/InstanceAccount.vala -src/MainWindow.vala -src/Network.vala -src/Watchlist.vala -src/Notificator.vala -src/Settings.vala +src/Services/Notificator.vala +src/Services/Settings.vala +src/Services/Accounts.vala +src/Services/IAccountListener.vala +src/Services/Cache.vala +src/Services/Network.vala +src/Services/Watchlist.vala src/API/Account.vala -src/API/Attachment.vala +src/API/Relationship.vala src/API/Mention.vala +src/API/Tag.vala +src/API/Status.vala +src/API/Visibility.vala src/API/Notification.vala src/API/NotificationType.vala -src/API/Relationship.vala -src/API/Status.vala -src/API/StatusVisibility.vala +src/API/Attachment.vala +src/Widgets/Avatar.vala src/Widgets/AccountsButton.vala -src/Widgets/AccountWidget.vala src/Widgets/AlignedLabel.vala -src/Widgets/AttachmentBox.vala -src/Widgets/AttachmentWidget.vala -src/Widgets/ImageToggleButton.vala -src/Widgets/NotificationWidget.vala src/Widgets/RichLabel.vala -src/Widgets/StatusWidget.vala -src/Dialogs/NewAccountDialog.vala -src/Dialogs/PostDialog.vala -src/Dialogs/SettingsDialog.vala -src/Dialogs/WatchlistDialog.vala -src/Views/AbstractView.vala -src/Views/AccountView.vala -src/Views/FavoritesView.vala -src/Views/DirectView.vala -src/Views/FederatedView.vala -src/Views/FollowersView.vala -src/Views/FollowingView.vala -src/Views/HomeView.vala -src/Views/LocalView.vala -src/Views/NotificationsView.vala -src/Views/SearchView.vala -src/Views/StatusView.vala -src/Views/TimelineView.vala +src/Widgets/Status.vala +src/Widgets/Notification.vala +src/Widgets/VisibilityPopover.vala +src/Widgets/Attachment/Box.vala +src/Widgets/Attachment/Item.vala +src/Dialogs/ISavedWindow.vala +src/Dialogs/MainWindow.vala +src/Dialogs/Compose.vala +src/Dialogs/Preferences.vala +src/Dialogs/WatchlistEditor.vala +src/Views/Base.vala +src/Views/NewAccount.vala +src/Views/Timeline.vala +src/Views/Home.vala +src/Views/Local.vala +src/Views/Federated.vala +src/Views/Notifications.vala +src/Views/Direct.vala +src/Views/ExpandedStatus.vala +src/Views/Profile.vala +src/Views/Favorites.vala +src/Views/Search.vala +src/Views/Hashtag.vala diff --git a/po/com.github.bleakgrey.tootle.pot b/po/com.github.bleakgrey.tootle.pot index fad4e60..622feac 100644 --- a/po/com.github.bleakgrey.tootle.pot +++ b/po/com.github.bleakgrey.tootle.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.bleakgrey.tootle\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-10-30 19:17+0300\n" +"POT-Creation-Date: 2019-09-16 16:00+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #: data/com.github.bleakgrey.tootle.desktop.in:4 -#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 src/MainWindow.vala:68 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 msgid "Tootle" msgstr "" @@ -42,366 +42,300 @@ msgstr "" #: data/com.github.bleakgrey.tootle.appdata.xml.in:11 msgid "" "Tootle is a client for the world’s largest free, open-source, decentralized " -"microblogging network with real-time notifications and multiple accounts " -"support." +"microblogging network with real-time notifications and support for multiple " +"accounts." msgstr "" #: data/com.github.bleakgrey.tootle.appdata.xml.in:14 msgid "" -"Mastodon is lovely crafted with power and speed in mind, resulting in a " -"free, independent and popular alternative to the centralized social networks." +"Mastodon is lovingly crafted with power and speed in mind, resulting in a " +"free, independent, and popular alternative to the centralized social " +"networks." msgstr "" #: data/com.github.bleakgrey.tootle.appdata.xml.in:17 msgid "" -"Anyone can run a server of Mastodon. Each server hosts individual user " -"accounts, the content they produce, and the content they are subscribed. " -"Every user can follow each other and share their posts regardless of their " -"server." +"Anyone can run a Mastodon server. Each server hosts individual user " +"accounts, the content they produce, and the content to which they are " +"subscribed. Every user can follow each other and share their posts " +"regardless of their server." msgstr "" #: data/com.github.bleakgrey.tootle.appdata.xml.in:26 msgid "bleak_grey" msgstr "" -#: src/Desktop.vala:10 src/API/Account.vala:123 src/API/Account.vala:142 -#: src/API/Account.vala:161 src/Dialogs/NewAccountDialog.vala:102 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:80 +msgid "Added Watchlist" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:81 +msgid "Added Redraft support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:82 +msgid "Added Pinning support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:83 +msgid "Added Simplified Chinese and German translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:84 +msgid "Added --hidden Start Flag" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:85 +msgid "Added Shortcuts and Back mouse button support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:86 +msgid "Changed Notifications screen behavior" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:87 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:102 +msgid "Fixed minor bugs" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:94 +msgid "Added Russian, French and Polish translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:95 +msgid "Added Direct timeline" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:96 +msgid "Added support for custom character limit" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:97 +msgid "Added support for streaming all timelines" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:98 +msgid "Added tooltips for image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:99 +msgid "Added remove action for attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:100 +msgid "Changed behavior for mentioning users" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:101 +msgid "Changed behavior for missing image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:109 +msgid "Initial release" +msgstr "" + +#: src/Desktop.vala:10 msgid "Error" msgstr "" -#: src/Desktop.vala:46 +#: src/Desktop.vala:47 msgid "Media downloaded" msgstr "" -#: src/MainWindow.vala:48 -msgid "Back" -msgstr "" - -#: src/MainWindow.vala:54 src/Dialogs/PostDialog.vala:29 -msgid "Toot" -msgstr "" - -#: src/Network.vala:58 -msgid "TLS Error" -msgstr "" - -#: src/Network.vala:58 -msgid "Can't ensure secure connection: " -msgstr "" - -#: src/Network.vala:66 +#: src/Services/Accounts.vala:31 #, c-format -msgid "Error: %s" +msgid "" +"This instance has invalidated this session. Please sign in again.\n" +"\n" +"%s" msgstr "" -#: src/API/NotificationType.vala:50 -#, c-format -msgid "%s mentioned you" +#: src/Services/Network.vala:86 +msgid "Network Error" msgstr "" -#: src/API/NotificationType.vala:52 -#, c-format -msgid "%s boosted your toot" +#: src/API/Visibility.vala:36 +msgid "Unlisted" msgstr "" -#: src/API/NotificationType.vala:54 -#, c-format -msgid "%s favorited your toot" +#: src/API/Visibility.vala:38 +msgid "Followers-only" +msgstr "" + +#: src/API/Visibility.vala:40 +msgid "Direct" +msgstr "" + +#: src/API/Visibility.vala:42 +msgid "Public" +msgstr "" + +#: src/API/Visibility.vala:49 +msgid "Don't post to public timelines" +msgstr "" + +#: src/API/Visibility.vala:51 +msgid "Post to followers only" +msgstr "" + +#: src/API/Visibility.vala:53 +msgid "Post to mentioned users only" +msgstr "" + +#: src/API/Visibility.vala:55 +msgid "Post to public timelines" msgstr "" #: src/API/NotificationType.vala:56 #, c-format -msgid "%s now follows you" +msgid "" +"%s mentioned you" msgstr "" #: src/API/NotificationType.vala:58 #, c-format -msgid "%s wants to follow you" +msgid "" +"%s boosted your status" msgstr "" #: src/API/NotificationType.vala:60 #, c-format -msgid "%s posted a toot" +msgid "%s boosted" msgstr "" -#: src/API/Status.vala:174 -msgid "Boosted!" +#: src/API/NotificationType.vala:62 +#, c-format +msgid "" +"%s favorited your status" msgstr "" -#: src/API/Status.vala:176 -msgid "Removed boost" +#: src/API/NotificationType.vala:64 +#, c-format +msgid "" +"%s now follows you" msgstr "" -#: src/API/Status.vala:189 -msgid "Favorited!" +#: src/API/NotificationType.vala:66 +#, c-format +msgid "" +"%s wants to follow you" msgstr "" -#: src/API/Status.vala:191 -msgid "Removed from favorites" +#: src/API/NotificationType.vala:68 +#, c-format +msgid "" +"%s posted a status" msgstr "" -#: src/API/Status.vala:204 -msgid "Muted!" -msgstr "" - -#: src/API/Status.vala:206 -msgid "Conversation unmuted" -msgstr "" - -#: src/API/Status.vala:219 -msgid "Pinned!" -msgstr "" - -#: src/API/Status.vala:221 -msgid "Unpinned from profile" -msgstr "" - -#: src/API/Status.vala:231 -msgid "Poof!" -msgstr "" - -#: src/API/StatusVisibility.vala:40 -msgid "Post to public timelines" -msgstr "" - -#: src/API/StatusVisibility.vala:42 -msgid "Don't post to public timelines" -msgstr "" - -#: src/API/StatusVisibility.vala:44 -msgid "Post to followers only" -msgstr "" - -#: src/API/StatusVisibility.vala:46 -msgid "Post to mentioned users only" -msgstr "" - -#: src/Widgets/AccountsButton.vala:67 -msgid "Refresh" -msgstr "" - -#: src/Widgets/AccountsButton.vala:71 -msgid "Favorites" -msgstr "" - -#: src/Widgets/AccountsButton.vala:75 src/Views/DirectView.vala:12 -msgid "Direct Messages" -msgstr "" - -#: src/Widgets/AccountsButton.vala:79 src/Views/SearchView.vala:12 -msgid "Search" -msgstr "" - -#: src/Widgets/AccountsButton.vala:83 -msgid "Watchlist" -msgstr "" - -#: src/Widgets/AccountsButton.vala:87 src/Dialogs/SettingsDialog.vala:18 -msgid "Settings" -msgstr "" - -#: src/Widgets/AccountsButton.vala:142 -msgid "New Account" -msgstr "" - -#: src/Widgets/AccountsButton.vala:143 -msgid "Click to add" -msgstr "" - -#: src/Widgets/AccountWidget.vala:24 src/Widgets/AttachmentWidget.vala:130 -#: src/Widgets/StatusWidget.vala:289 +#: src/Widgets/RichLabel.vala:105 src/Widgets/Status.vala:206 +#: src/Widgets/Account.vala:28 msgid "Open in Browser" msgstr "" -#: src/Widgets/AccountWidget.vala:26 src/Widgets/AttachmentWidget.vala:132 -#: src/Widgets/StatusWidget.vala:291 -msgid "Copy Link" +#: src/Widgets/AccountsButton.vala:62 +msgid "Refresh" msgstr "" -#: src/Widgets/AttachmentBox.vala:41 -msgid "Select media files to add" +#: src/Widgets/AccountsButton.vala:67 +msgid "Favorites" msgstr "" -#: src/Widgets/AttachmentBox.vala:44 -msgid "_Cancel" +#: src/Widgets/AccountsButton.vala:71 src/Views/Direct.vala:12 +msgid "Direct Messages" msgstr "" -#: src/Widgets/AttachmentBox.vala:46 -msgid "_Open" +#: src/Widgets/AccountsButton.vala:75 src/Views/Search.vala:12 +msgid "Search" msgstr "" -#: src/Widgets/AttachmentWidget.vala:67 -#, c-format -msgid "Click to open %s media" +#: src/Widgets/AccountsButton.vala:79 src/Dialogs/WatchlistEditor.vala:99 +msgid "Watchlist" msgstr "" -#: src/Widgets/AttachmentWidget.vala:84 -msgid "Uploading..." +#: src/Widgets/AccountsButton.vala:83 src/Dialogs/Preferences.vala:17 +msgid "Settings" msgstr "" -#: src/Widgets/AttachmentWidget.vala:105 -msgid "File read error" +#: src/Widgets/Status.vala:52 +msgid "[ Show more ]" msgstr "" -#: src/Widgets/AttachmentWidget.vala:105 -#, c-format -msgid "Can't read file %s: %s" -msgstr "" - -#: src/Widgets/AttachmentWidget.vala:124 -msgid "Remove" -msgstr "" - -#: src/Widgets/AttachmentWidget.vala:134 -msgid "Download" -msgstr "" - -#: src/Widgets/NotificationWidget.vala:20 -msgid "Unknown Notification" -msgstr "" - -#: src/Widgets/NotificationWidget.vala:25 -msgid "Dismiss" -msgstr "" - -#: src/Widgets/NotificationWidget.vala:64 -msgid "Accept" -msgstr "" - -#: src/Widgets/NotificationWidget.vala:66 -msgid "Reject" -msgstr "" - -#: src/Widgets/StatusWidget.vala:84 -msgid "Boost" -msgstr "" - -#: src/Widgets/StatusWidget.vala:91 -msgid "Favorite" -msgstr "" - -#: src/Widgets/StatusWidget.vala:98 -msgid "Reply" -msgstr "" - -#: src/Widgets/StatusWidget.vala:136 -#, c-format -msgid "%s boosted" -msgstr "" - -#: src/Widgets/StatusWidget.vala:151 -msgid "Toggle content" -msgstr "" - -#: src/Widgets/StatusWidget.vala:165 -msgid "[ This post contains sensitive content ]" -msgstr "" - -#: src/Widgets/StatusWidget.vala:234 +#: src/Widgets/Status.vala:120 msgid "This post can't be boosted" msgstr "" -#: src/Widgets/StatusWidget.vala:287 -msgid "Unmute Conversation" +#: src/Widgets/Status.vala:208 src/Widgets/Account.vala:30 +msgid "Copy Link" msgstr "" -#: src/Widgets/StatusWidget.vala:287 -msgid "Mute Conversation" -msgstr "" - -#: src/Widgets/StatusWidget.vala:293 +#: src/Widgets/Status.vala:210 msgid "Copy Text" msgstr "" -#: src/Widgets/StatusWidget.vala:300 +#: src/Widgets/Status.vala:217 msgid "Unpin from Profile" msgstr "" -#: src/Widgets/StatusWidget.vala:300 +#: src/Widgets/Status.vala:217 msgid "Pin on Profile" msgstr "" -#: src/Widgets/StatusWidget.vala:304 +#: src/Widgets/Status.vala:221 msgid "Delete" msgstr "" -#: src/Widgets/StatusWidget.vala:308 src/Dialogs/PostDialog.vala:72 +#: src/Widgets/Status.vala:225 src/Dialogs/Compose.vala:73 msgid "Redraft" msgstr "" -#: src/Dialogs/NewAccountDialog.vala:27 -msgid "New Account" +#: src/Widgets/Attachment/Box.vala:28 +msgid "Select media files to add" msgstr "" -#: src/Dialogs/NewAccountDialog.vala:38 -msgid "What's an instance?" +#: src/Widgets/Attachment/Box.vala:31 +msgid "_Cancel" msgstr "" -#: src/Dialogs/NewAccountDialog.vala:42 -msgid "Code:" +#: src/Widgets/Attachment/Box.vala:33 +msgid "_Open" msgstr "" -#: src/Dialogs/NewAccountDialog.vala:46 -msgid "Paste your instance authorization code here" +#: src/Dialogs/MainWindow.vala:49 +msgid "Back" msgstr "" -#: src/Dialogs/NewAccountDialog.vala:49 -msgid "Add Account" +#: src/Dialogs/MainWindow.vala:58 +msgid "Toot" msgstr "" -#: src/Dialogs/NewAccountDialog.vala:60 -msgid "Instance:" +#: src/Dialogs/MainWindow.vala:63 +msgid "Toggle content" msgstr "" -#: src/Dialogs/NewAccountDialog.vala:102 -msgid "Please paste valid instance authorization code" +#: src/Dialogs/Compose.vala:69 +msgid "Post" msgstr "" -#: src/Dialogs/NewAccountDialog.vala:110 -msgid "Network Error" -msgstr "" - -#: src/Dialogs/PostDialog.vala:45 -msgid "Post Visibility" -msgstr "" - -#: src/Dialogs/PostDialog.vala:52 -msgid "Add Media" -msgstr "" - -#: src/Dialogs/PostDialog.vala:61 -msgid "Spoiler Warning" -msgstr "" - -#: src/Dialogs/PostDialog.vala:68 -msgid "Cancel" -msgstr "" - -#: src/Dialogs/PostDialog.vala:77 -msgid "Toot!" -msgstr "" - -#: src/Dialogs/PostDialog.vala:85 -msgid "Write your warning here" -msgstr "" - -#: src/Dialogs/SettingsDialog.vala:37 +#: src/Dialogs/Preferences.vala:36 msgid "Appearance" msgstr "" -#: src/Dialogs/SettingsDialog.vala:38 +#: src/Dialogs/Preferences.vala:37 msgid "Dark theme:" msgstr "" -#: src/Dialogs/SettingsDialog.vala:41 +#: src/Dialogs/Preferences.vala:40 msgid "Timelines" msgstr "" -#: src/Dialogs/SettingsDialog.vala:42 +#: src/Dialogs/Preferences.vala:41 msgid "Real-time updates:" msgstr "" -#: src/Dialogs/SettingsDialog.vala:44 +#: src/Dialogs/Preferences.vala:43 msgid "Update public timelines:" msgstr "" @@ -409,140 +343,115 @@ msgstr "" #. grid.attach (new SettingsLabel (_("Use cache:")), 0, i); #. grid.attach (new SettingsSwitch ("cache"), 1, i++); #. grid.attach (new SettingsLabel (_("Max cache size (MB):")), 0, i); -#. var cache_size = new Gtk.SpinButton.with_range (16, 256, 1); +#. var cache_size = new SpinButton.with_range (16, 256, 1); #. settings.schema.bind ("cache-size", cache_size, "value", SettingsBindFlags.DEFAULT); #. grid.attach (cache_size, 1, i++); -#: src/Dialogs/SettingsDialog.vala:55 src/Views/NotificationsView.vala:34 +#: src/Dialogs/Preferences.vala:54 src/Views/Notifications.vala:33 msgid "Notifications" msgstr "" -#: src/Dialogs/SettingsDialog.vala:56 +#: src/Dialogs/Preferences.vala:55 msgid "Display notifications:" msgstr "" -#: src/Dialogs/SettingsDialog.vala:58 +#: src/Dialogs/Preferences.vala:57 msgid "Always receive notifications:" msgstr "" -#: src/Dialogs/SettingsDialog.vala:64 +#: src/Dialogs/Preferences.vala:63 src/Dialogs/WatchlistEditor.vala:162 msgid "_Close" msgstr "" -#: src/Dialogs/WatchlistDialog.vala:20 +#: src/Dialogs/WatchlistEditor.vala:20 msgid "" "You'll be notified when toots from this user appear in your Home timeline." msgstr "" -#: src/Dialogs/WatchlistDialog.vala:21 +#: src/Dialogs/WatchlistEditor.vala:21 msgid "" "You'll be notified when toots with this hashtag appear in any public " "timelines." msgstr "" -#: src/Dialogs/WatchlistDialog.vala:137 +#: src/Dialogs/WatchlistEditor.vala:108 msgid "Users" msgstr "" -#: src/Dialogs/WatchlistDialog.vala:138 src/Views/SearchView.vala:100 +#: src/Dialogs/WatchlistEditor.vala:109 src/Views/Search.vala:100 msgid "Hashtags" msgstr "" -#: src/Dialogs/WatchlistDialog.vala:148 +#: src/Dialogs/WatchlistEditor.vala:122 msgid "Add" msgstr "" -#: src/Views/AbstractView.vala:59 +#: src/Views/Base.vala:6 msgid "Nothing to see here" msgstr "" -#: src/Views/AccountView.vala:79 -msgid "Edit Profile" +#: src/Views/NewAccount.vala:91 +msgid "Instance URL is invalid" msgstr "" -#: src/Views/AccountView.vala:80 -msgid "Mention" +#: src/Views/NewAccount.vala:133 +msgid "Please paste a valid authorization code" msgstr "" -#: src/Views/AccountView.vala:81 -msgid "Report" -msgstr "" - -#: src/Views/AccountView.vala:82 src/Views/AccountView.vala:167 -msgid "Mute" -msgstr "" - -#: src/Views/AccountView.vala:83 src/Views/AccountView.vala:166 -msgid "Block" -msgstr "" - -#: src/Views/AccountView.vala:95 -msgid "More Actions" -msgstr "" - -#: src/Views/AccountView.vala:115 -msgid "Toots" -msgstr "" - -#: src/Views/AccountView.vala:116 -msgid "Follows" -msgstr "" - -#: src/Views/AccountView.vala:120 -msgid "Followers" -msgstr "" - -#: src/Views/AccountView.vala:155 -msgid "Unfollow" -msgstr "" - -#: src/Views/AccountView.vala:159 -msgid "Follow" -msgstr "" - -#: src/Views/AccountView.vala:166 -msgid "Unblock" -msgstr "" - -#: src/Views/AccountView.vala:167 -msgid "Unmute" -msgstr "" - -#: src/Views/AccountView.vala:228 -msgid "Sent follow request" -msgstr "" - -#: src/Views/AccountView.vala:230 -msgid "Blocked" -msgstr "" - -#: src/Views/AccountView.vala:232 -msgid "Follows you" -msgstr "" - -#: src/Views/AccountView.vala:234 -msgid "Blocking this instance" -msgstr "" - -#: src/Views/AccountView.vala:269 -msgid "User not found" -msgstr "" - -#: src/Views/FederatedView.vala:12 -msgid "Federated Timeline" -msgstr "" - -#: src/Views/HomeView.vala:12 src/Views/TimelineView.vala:36 +#: src/Views/Timeline.vala:34 src/Views/Home.vala:12 msgid "Home" msgstr "" -#: src/Views/LocalView.vala:12 +#: src/Views/Local.vala:12 msgid "Local Timeline" msgstr "" -#: src/Views/SearchView.vala:82 +#: src/Views/Federated.vala:12 +msgid "Federated Timeline" +msgstr "" + +#: src/Views/Profile.vala:61 +#, c-format +msgid "%s Posts" +msgstr "" + +#: src/Views/Profile.vala:67 +#, c-format +msgid "%s Follows" +msgstr "" + +#: src/Views/Profile.vala:73 +#, c-format +msgid "%s Followers" +msgstr "" + +#: src/Views/Profile.vala:109 +msgid "Sent follow request" +msgstr "" + +#: src/Views/Profile.vala:111 +msgid "Mutually follows you" +msgstr "" + +#: src/Views/Profile.vala:113 +msgid "Follows you" +msgstr "" + +#: src/Views/Profile.vala:124 +msgid "Follow back" +msgstr "" + +#: src/Views/Profile.vala:126 +msgid "Unfollow" +msgstr "" + +#: src/Views/Profile.vala:128 +msgid "Follow" +msgstr "" + +#: src/Views/Search.vala:82 msgid "Accounts" msgstr "" -#: src/Views/SearchView.vala:91 +#: src/Views/Search.vala:91 msgid "Statuses" msgstr "" diff --git a/po/de_DE.po b/po/de_DE.po index e1065c6..e6a0bc5 100644 --- a/po/de_DE.po +++ b/po/de_DE.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.bleakgrey.tootle\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-10-30 19:17+0300\n" +"POT-Creation-Date: 2019-09-16 16:00+0300\n" "PO-Revision-Date: 2018-10-30 22:20+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -19,7 +19,7 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: data/com.github.bleakgrey.tootle.desktop.in:4 -#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 src/MainWindow.vala:68 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 msgid "Tootle" msgstr "Tootle" @@ -41,30 +41,34 @@ msgid "Lightning fast client for Mastodon" msgstr "Blitzschneller Client für Mastodon" #: data/com.github.bleakgrey.tootle.appdata.xml.in:11 +#, fuzzy msgid "" "Tootle is a client for the world’s largest free, open-source, decentralized " -"microblogging network with real-time notifications and multiple accounts " -"support." +"microblogging network with real-time notifications and support for multiple " +"accounts." msgstr "" "Tootle ist ein Client für das weltgrößte freie, dezentrale, open-source " "Microblogging-Netzwerk mit Echtzeit-Benachrichtigungen und Multi-Account-" "Support." #: data/com.github.bleakgrey.tootle.appdata.xml.in:14 +#, fuzzy msgid "" -"Mastodon is lovely crafted with power and speed in mind, resulting in a " -"free, independent and popular alternative to the centralized social networks." +"Mastodon is lovingly crafted with power and speed in mind, resulting in a " +"free, independent, and popular alternative to the centralized social " +"networks." msgstr "" "Mastodon ist eine liebevoll erstellte schnelle und kraftvolle Software, die " "frei und unabhängig ist und eine populäre Alternative zu zentralisierten " "sozialen Netzwerken ist." #: data/com.github.bleakgrey.tootle.appdata.xml.in:17 +#, fuzzy msgid "" -"Anyone can run a server of Mastodon. Each server hosts individual user " -"accounts, the content they produce, and the content they are subscribed. " -"Every user can follow each other and share their posts regardless of their " -"server." +"Anyone can run a Mastodon server. Each server hosts individual user " +"accounts, the content they produce, and the content to which they are " +"subscribed. Every user can follow each other and share their posts " +"regardless of their server." msgstr "" "Jeder kann sein eigenes Mastodon betreiben. Jeder Server hostet seine " "eigenen Nutzerkonten, den Inhalt, den sie erstellen und den Inhalt, den sie " @@ -75,344 +79,280 @@ msgstr "" msgid "bleak_grey" msgstr "bleak_grey" -#: src/Desktop.vala:10 src/API/Account.vala:123 src/API/Account.vala:142 -#: src/API/Account.vala:161 src/Dialogs/NewAccountDialog.vala:102 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:80 +#, fuzzy +msgid "Added Watchlist" +msgstr "Beobachtungsliste" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:81 +msgid "Added Redraft support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:82 +msgid "Added Pinning support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:83 +msgid "Added Simplified Chinese and German translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:84 +msgid "Added --hidden Start Flag" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:85 +msgid "Added Shortcuts and Back mouse button support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:86 +msgid "Changed Notifications screen behavior" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:87 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:102 +msgid "Fixed minor bugs" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:94 +msgid "Added Russian, French and Polish translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:95 +#, fuzzy +msgid "Added Direct timeline" +msgstr "Aktualisiere öffentliche Zeitleisten:" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:96 +msgid "Added support for custom character limit" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:97 +msgid "Added support for streaming all timelines" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:98 +msgid "Added tooltips for image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:99 +msgid "Added remove action for attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:100 +msgid "Changed behavior for mentioning users" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:101 +msgid "Changed behavior for missing image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:109 +msgid "Initial release" +msgstr "" + +#: src/Desktop.vala:10 msgid "Error" msgstr "Fehler" -#: src/Desktop.vala:46 +#: src/Desktop.vala:47 msgid "Media downloaded" msgstr "Medien heruntergeladen" -#: src/MainWindow.vala:48 -msgid "Back" -msgstr "Zurück" - -#: src/MainWindow.vala:54 src/Dialogs/PostDialog.vala:29 -msgid "Toot" -msgstr "Tröt" - -#: src/Network.vala:58 -msgid "TLS Error" -msgstr "TLS-Fehler" - -#: src/Network.vala:58 -msgid "Can't ensure secure connection: " -msgstr "Kann keine sichere Verbindung aufbauen: " - -#: src/Network.vala:66 +#: src/Services/Accounts.vala:31 #, c-format -msgid "Error: %s" -msgstr "Fehler: %s" +msgid "" +"This instance has invalidated this session. Please sign in again.\n" +"\n" +"%s" +msgstr "" -#: src/API/NotificationType.vala:50 -#, c-format -msgid "%s mentioned you" -msgstr "%s hat dich erwähnt" - -#: src/API/NotificationType.vala:52 -#, c-format -msgid "%s boosted your toot" -msgstr "%s hat deinen Beitrag geteilt" - -#: src/API/NotificationType.vala:54 -#, c-format -msgid "%s favorited your toot" -msgstr "%s hat deinen Beitrag favorisiert" - -#: src/API/NotificationType.vala:56 -#, c-format -msgid "%s now follows you" -msgstr "%s folgt dir nun" - -#: src/API/NotificationType.vala:58 -#, c-format -msgid "%s wants to follow you" -msgstr "%s möchte dir folgen" - -#: src/API/NotificationType.vala:60 -#, c-format -msgid "%s posted a toot" -msgstr "%s hat einen Beitrag gepostet" - -#: src/API/Status.vala:174 -msgid "Boosted!" -msgstr "Geteilt!" - -#: src/API/Status.vala:176 -msgid "Removed boost" -msgstr "Boost entfernt" - -#: src/API/Status.vala:189 -msgid "Favorited!" -msgstr "Favorisiert!" - -#: src/API/Status.vala:191 -msgid "Removed from favorites" -msgstr "Von den Favoriten entfernt" - -#: src/API/Status.vala:204 -msgid "Muted!" -msgstr "Stummgeschaltet!" - -#: src/API/Status.vala:206 -msgid "Conversation unmuted" -msgstr "Konversation nicht mehr stummgeschaltet" - -#: src/API/Status.vala:219 -msgid "Pinned!" -msgstr "Angepinnt!" - -#: src/API/Status.vala:221 -msgid "Unpinned from profile" -msgstr "Vom Profil losgelöst" - -#: src/API/Status.vala:231 -msgid "Poof!" -msgstr "Poof!" - -#: src/API/StatusVisibility.vala:40 -msgid "Post to public timelines" -msgstr "An die öffentliche Zeitleiste posten" - -#: src/API/StatusVisibility.vala:42 -msgid "Don't post to public timelines" -msgstr "Nicht an die öffentliche Zeitleiste posten" - -#: src/API/StatusVisibility.vala:44 -msgid "Post to followers only" -msgstr "Nur für Follower posten" - -#: src/API/StatusVisibility.vala:46 -msgid "Post to mentioned users only" -msgstr "Nur für erwähnte Nutzer posten" - -#: src/Widgets/AccountsButton.vala:67 -msgid "Refresh" -msgstr "Aktualisieren" - -#: src/Widgets/AccountsButton.vala:71 -msgid "Favorites" -msgstr "Favoriten" - -#: src/Widgets/AccountsButton.vala:75 src/Views/DirectView.vala:12 -msgid "Direct Messages" -msgstr "Direktnachrichten" - -#: src/Widgets/AccountsButton.vala:79 src/Views/SearchView.vala:12 -msgid "Search" -msgstr "Suchen" - -#: src/Widgets/AccountsButton.vala:83 -msgid "Watchlist" -msgstr "Beobachtungsliste" - -#: src/Widgets/AccountsButton.vala:87 src/Dialogs/SettingsDialog.vala:18 -msgid "Settings" -msgstr "Einstellungen" - -#: src/Widgets/AccountsButton.vala:142 -msgid "New Account" -msgstr "Neuer Account" - -#: src/Widgets/AccountsButton.vala:143 -msgid "Click to add" -msgstr "Klicken zum Hinzufügen" - -#: src/Widgets/AccountWidget.vala:24 src/Widgets/AttachmentWidget.vala:130 -#: src/Widgets/StatusWidget.vala:289 -msgid "Open in Browser" -msgstr "Im Browser öffnen" - -#: src/Widgets/AccountWidget.vala:26 src/Widgets/AttachmentWidget.vala:132 -#: src/Widgets/StatusWidget.vala:291 -msgid "Copy Link" -msgstr "Link kopieren" - -#: src/Widgets/AttachmentBox.vala:41 -msgid "Select media files to add" -msgstr "Wähle Medien zum Senden aus" - -#: src/Widgets/AttachmentBox.vala:44 -msgid "_Cancel" -msgstr "_Cancel" - -#: src/Widgets/AttachmentBox.vala:46 -msgid "_Open" -msgstr "_Open" - -#: src/Widgets/AttachmentWidget.vala:67 -#, c-format -msgid "Click to open %s media" -msgstr "Klicken um %s Medien zu öffnen" - -#: src/Widgets/AttachmentWidget.vala:84 -msgid "Uploading..." -msgstr "Hochladen..." - -#: src/Widgets/AttachmentWidget.vala:105 -msgid "File read error" -msgstr "Dateilesefehler" - -#: src/Widgets/AttachmentWidget.vala:105 -#, c-format -msgid "Can't read file %s: %s" -msgstr "Kann Datei nicht lesen %s: %s" - -#: src/Widgets/AttachmentWidget.vala:124 -msgid "Remove" -msgstr "Entfernen" - -#: src/Widgets/AttachmentWidget.vala:134 -msgid "Download" -msgstr "Herunterladen" - -#: src/Widgets/NotificationWidget.vala:20 -msgid "Unknown Notification" -msgstr "Unbekannte Benachrichtigung" - -#: src/Widgets/NotificationWidget.vala:25 -msgid "Dismiss" -msgstr "Verwerfen" - -#: src/Widgets/NotificationWidget.vala:64 -msgid "Accept" -msgstr "Aktzeptieren" - -#: src/Widgets/NotificationWidget.vala:66 -msgid "Reject" -msgstr "Ablehnen" - -#: src/Widgets/StatusWidget.vala:84 -msgid "Boost" -msgstr "Teilen" - -#: src/Widgets/StatusWidget.vala:91 -msgid "Favorite" -msgstr "Favorisieren" - -#: src/Widgets/StatusWidget.vala:98 -msgid "Reply" -msgstr "Antworten" - -#: src/Widgets/StatusWidget.vala:136 -#, c-format -msgid "%s boosted" -msgstr "%s teilte" - -#: src/Widgets/StatusWidget.vala:151 -msgid "Toggle content" -msgstr "Umschalten" - -#: src/Widgets/StatusWidget.vala:165 -msgid "[ This post contains sensitive content ]" -msgstr "[ Dieser Beitrag beinhaltet sensibles Material ]" - -#: src/Widgets/StatusWidget.vala:234 -msgid "This post can't be boosted" -msgstr "Dieser Beitrag kann nicht geteilt werden" - -#: src/Widgets/StatusWidget.vala:287 -msgid "Unmute Conversation" -msgstr "Konversation nicht mehr stummschalten" - -#: src/Widgets/StatusWidget.vala:287 -msgid "Mute Conversation" -msgstr "Konversation stummschalten" - -#: src/Widgets/StatusWidget.vala:293 -msgid "Copy Text" -msgstr "Text kopieren" - -#: src/Widgets/StatusWidget.vala:300 -msgid "Unpin from Profile" -msgstr "Vom Profil loslösen" - -#: src/Widgets/StatusWidget.vala:300 -msgid "Pin on Profile" -msgstr "An Profil anheften" - -#: src/Widgets/StatusWidget.vala:304 -msgid "Delete" -msgstr "Löschen" - -#: src/Widgets/StatusWidget.vala:308 src/Dialogs/PostDialog.vala:72 -msgid "Redraft" -msgstr "Neu verfassen" - -#: src/Dialogs/NewAccountDialog.vala:27 -msgid "New Account" -msgstr "Neues Konto" - -#: src/Dialogs/NewAccountDialog.vala:38 -msgid "What's an instance?" -msgstr "Was ist eine Instanz?" - -#: src/Dialogs/NewAccountDialog.vala:42 -msgid "Code:" -msgstr "Code:" - -#: src/Dialogs/NewAccountDialog.vala:46 -msgid "Paste your instance authorization code here" -msgstr "Füge dein Autorisierungstoken hier ein" - -#: src/Dialogs/NewAccountDialog.vala:49 -msgid "Add Account" -msgstr "Konto hinzufügen" - -#: src/Dialogs/NewAccountDialog.vala:60 -msgid "Instance:" -msgstr "Instanz:" - -#: src/Dialogs/NewAccountDialog.vala:102 -msgid "Please paste valid instance authorization code" -msgstr "Bitte füge einen validen Autorisierungscode ein" - -#: src/Dialogs/NewAccountDialog.vala:110 +#: src/Services/Network.vala:86 msgid "Network Error" msgstr "Netzwerkfehler" -#: src/Dialogs/PostDialog.vala:45 -msgid "Post Visibility" -msgstr "Post-Sichtbarkeit" +#: src/API/Visibility.vala:36 +msgid "Unlisted" +msgstr "" -#: src/Dialogs/PostDialog.vala:52 -msgid "Add Media" -msgstr "Medien hinzufügen" +#: src/API/Visibility.vala:38 +#, fuzzy +msgid "Followers-only" +msgstr "Follower" -#: src/Dialogs/PostDialog.vala:61 -msgid "Spoiler Warning" -msgstr "Inhaltswarnung" +#: src/API/Visibility.vala:40 +msgid "Direct" +msgstr "" -#: src/Dialogs/PostDialog.vala:68 -msgid "Cancel" -msgstr "Abbrechen" +#: src/API/Visibility.vala:42 +msgid "Public" +msgstr "" -#: src/Dialogs/PostDialog.vala:77 -msgid "Toot!" -msgstr "Tröt!" +#: src/API/Visibility.vala:49 +msgid "Don't post to public timelines" +msgstr "Nicht an die öffentliche Zeitleiste posten" -#: src/Dialogs/PostDialog.vala:85 -msgid "Write your warning here" -msgstr "Schreibe deine Warnung hier" +#: src/API/Visibility.vala:51 +msgid "Post to followers only" +msgstr "Nur für Follower posten" -#: src/Dialogs/SettingsDialog.vala:37 +#: src/API/Visibility.vala:53 +msgid "Post to mentioned users only" +msgstr "Nur für erwähnte Nutzer posten" + +#: src/API/Visibility.vala:55 +msgid "Post to public timelines" +msgstr "An die öffentliche Zeitleiste posten" + +#: src/API/NotificationType.vala:56 +#, fuzzy, c-format +msgid "" +"%s mentioned you" +msgstr "%s hat dich erwähnt" + +#: src/API/NotificationType.vala:58 +#, fuzzy, c-format +msgid "" +"%s boosted your status" +msgstr "%s hat deinen Beitrag geteilt" + +#: src/API/NotificationType.vala:60 +#, fuzzy, c-format +msgid "%s boosted" +msgstr "%s teilte" + +#: src/API/NotificationType.vala:62 +#, fuzzy, c-format +msgid "" +"%s favorited your status" +msgstr "%s hat deinen Beitrag favorisiert" + +#: src/API/NotificationType.vala:64 +#, fuzzy, c-format +msgid "" +"%s now follows you" +msgstr "%s folgt dir nun" + +#: src/API/NotificationType.vala:66 +#, fuzzy, c-format +msgid "" +"%s wants to follow you" +msgstr "%s möchte dir folgen" + +#: src/API/NotificationType.vala:68 +#, fuzzy, c-format +msgid "" +"%s posted a status" +msgstr "%s hat einen Beitrag gepostet" + +#: src/Widgets/RichLabel.vala:105 src/Widgets/Status.vala:206 +#: src/Widgets/Account.vala:28 +msgid "Open in Browser" +msgstr "Im Browser öffnen" + +#: src/Widgets/AccountsButton.vala:62 +msgid "Refresh" +msgstr "Aktualisieren" + +#: src/Widgets/AccountsButton.vala:67 +msgid "Favorites" +msgstr "Favoriten" + +#: src/Widgets/AccountsButton.vala:71 src/Views/Direct.vala:12 +msgid "Direct Messages" +msgstr "Direktnachrichten" + +#: src/Widgets/AccountsButton.vala:75 src/Views/Search.vala:12 +msgid "Search" +msgstr "Suchen" + +#: src/Widgets/AccountsButton.vala:79 src/Dialogs/WatchlistEditor.vala:99 +msgid "Watchlist" +msgstr "Beobachtungsliste" + +#: src/Widgets/AccountsButton.vala:83 src/Dialogs/Preferences.vala:17 +msgid "Settings" +msgstr "Einstellungen" + +#: src/Widgets/Status.vala:52 +msgid "[ Show more ]" +msgstr "" + +#: src/Widgets/Status.vala:120 +msgid "This post can't be boosted" +msgstr "Dieser Beitrag kann nicht geteilt werden" + +#: src/Widgets/Status.vala:208 src/Widgets/Account.vala:30 +msgid "Copy Link" +msgstr "Link kopieren" + +#: src/Widgets/Status.vala:210 +msgid "Copy Text" +msgstr "Text kopieren" + +#: src/Widgets/Status.vala:217 +msgid "Unpin from Profile" +msgstr "Vom Profil loslösen" + +#: src/Widgets/Status.vala:217 +msgid "Pin on Profile" +msgstr "An Profil anheften" + +#: src/Widgets/Status.vala:221 +msgid "Delete" +msgstr "Löschen" + +#: src/Widgets/Status.vala:225 src/Dialogs/Compose.vala:73 +msgid "Redraft" +msgstr "Neu verfassen" + +#: src/Widgets/Attachment/Box.vala:28 +msgid "Select media files to add" +msgstr "Wähle Medien zum Senden aus" + +#: src/Widgets/Attachment/Box.vala:31 +msgid "_Cancel" +msgstr "_Cancel" + +#: src/Widgets/Attachment/Box.vala:33 +msgid "_Open" +msgstr "_Open" + +#: src/Dialogs/MainWindow.vala:49 +msgid "Back" +msgstr "Zurück" + +#: src/Dialogs/MainWindow.vala:58 +msgid "Toot" +msgstr "Tröt" + +#: src/Dialogs/MainWindow.vala:63 +msgid "Toggle content" +msgstr "Umschalten" + +#: src/Dialogs/Compose.vala:69 +msgid "Post" +msgstr "" + +#: src/Dialogs/Preferences.vala:36 msgid "Appearance" msgstr "Aussehen" -#: src/Dialogs/SettingsDialog.vala:38 +#: src/Dialogs/Preferences.vala:37 msgid "Dark theme:" msgstr "Dunkles Design:" -#: src/Dialogs/SettingsDialog.vala:41 +#: src/Dialogs/Preferences.vala:40 msgid "Timelines" msgstr "Zeitleisten" -#: src/Dialogs/SettingsDialog.vala:42 +#: src/Dialogs/Preferences.vala:41 msgid "Real-time updates:" msgstr "Echtzeit-Aktualisierungen:" -#: src/Dialogs/SettingsDialog.vala:44 +#: src/Dialogs/Preferences.vala:43 msgid "Update public timelines:" msgstr "Aktualisiere öffentliche Zeitleisten:" @@ -420,33 +360,33 @@ msgstr "Aktualisiere öffentliche Zeitleisten:" #. grid.attach (new SettingsLabel (_("Use cache:")), 0, i); #. grid.attach (new SettingsSwitch ("cache"), 1, i++); #. grid.attach (new SettingsLabel (_("Max cache size (MB):")), 0, i); -#. var cache_size = new Gtk.SpinButton.with_range (16, 256, 1); +#. var cache_size = new SpinButton.with_range (16, 256, 1); #. settings.schema.bind ("cache-size", cache_size, "value", SettingsBindFlags.DEFAULT); #. grid.attach (cache_size, 1, i++); -#: src/Dialogs/SettingsDialog.vala:55 src/Views/NotificationsView.vala:34 +#: src/Dialogs/Preferences.vala:54 src/Views/Notifications.vala:33 msgid "Notifications" msgstr "Benachrichtigungen" -#: src/Dialogs/SettingsDialog.vala:56 +#: src/Dialogs/Preferences.vala:55 msgid "Display notifications:" msgstr "Benachrichtigungen anzeigen:" -#: src/Dialogs/SettingsDialog.vala:58 +#: src/Dialogs/Preferences.vala:57 msgid "Always receive notifications:" msgstr "Immer Benachrichtigungen empfangen:" -#: src/Dialogs/SettingsDialog.vala:64 +#: src/Dialogs/Preferences.vala:63 src/Dialogs/WatchlistEditor.vala:162 msgid "_Close" msgstr "_Close" -#: src/Dialogs/WatchlistDialog.vala:20 +#: src/Dialogs/WatchlistEditor.vala:20 msgid "" "You'll be notified when toots from this user appear in your Home timeline." msgstr "" "Du wirst benachrichtigt wenn Beiträge von dem Nutzer in deiner Startseite " "erscheinen." -#: src/Dialogs/WatchlistDialog.vala:21 +#: src/Dialogs/WatchlistEditor.vala:21 msgid "" "You'll be notified when toots with this hashtag appear in any public " "timelines." @@ -454,114 +394,254 @@ msgstr "" "Du wirst benachrichtigt wenn Beiträge mit diesem Hashtag in irgendwelchen " "öffentlichen Zeitleisten geteilt werden." -#: src/Dialogs/WatchlistDialog.vala:137 +#: src/Dialogs/WatchlistEditor.vala:108 msgid "Users" msgstr "Benutzer" -#: src/Dialogs/WatchlistDialog.vala:138 src/Views/SearchView.vala:100 +#: src/Dialogs/WatchlistEditor.vala:109 src/Views/Search.vala:100 msgid "Hashtags" msgstr "Hashtags" -#: src/Dialogs/WatchlistDialog.vala:148 +#: src/Dialogs/WatchlistEditor.vala:122 msgid "Add" msgstr "Hinzufügen" -#: src/Views/AbstractView.vala:59 +#: src/Views/Base.vala:6 msgid "Nothing to see here" msgstr "Hier gibt es nichts zu sehen" -#: src/Views/AccountView.vala:79 -msgid "Edit Profile" -msgstr "Profil bearbeiten" +#: src/Views/NewAccount.vala:91 +msgid "Instance URL is invalid" +msgstr "" -#: src/Views/AccountView.vala:80 -msgid "Mention" -msgstr "Erwähnen" +#: src/Views/NewAccount.vala:133 +#, fuzzy +msgid "Please paste a valid authorization code" +msgstr "Bitte füge einen validen Autorisierungscode ein" -#: src/Views/AccountView.vala:81 -msgid "Report" -msgstr "Melden" - -#: src/Views/AccountView.vala:82 src/Views/AccountView.vala:167 -msgid "Mute" -msgstr "Stummschalten" - -#: src/Views/AccountView.vala:83 src/Views/AccountView.vala:166 -msgid "Block" -msgstr "Blockieren" - -#: src/Views/AccountView.vala:95 -msgid "More Actions" -msgstr "Weitere Aktionen" - -#: src/Views/AccountView.vala:115 -msgid "Toots" -msgstr "Beiträge" - -#: src/Views/AccountView.vala:116 -msgid "Follows" -msgstr "Folgt" - -#: src/Views/AccountView.vala:120 -msgid "Followers" -msgstr "Follower" - -#: src/Views/AccountView.vala:155 -msgid "Unfollow" -msgstr "Entfolgen" - -#: src/Views/AccountView.vala:159 -msgid "Follow" -msgstr "Folgen" - -#: src/Views/AccountView.vala:166 -msgid "Unblock" -msgstr "Entsperren" - -#: src/Views/AccountView.vala:167 -msgid "Unmute" -msgstr "Nicht mehr stummschalten" - -#: src/Views/AccountView.vala:228 -msgid "Sent follow request" -msgstr "Followanfrage gesendet" - -#: src/Views/AccountView.vala:230 -msgid "Blocked" -msgstr "Blockiert" - -#: src/Views/AccountView.vala:232 -msgid "Follows you" -msgstr "Folgt dir" - -#: src/Views/AccountView.vala:234 -msgid "Blocking this instance" -msgstr "Diese Instanz blockieren" - -#: src/Views/AccountView.vala:269 -msgid "User not found" -msgstr "Benutzer nicht gefunden" - -#: src/Views/FederatedView.vala:12 -msgid "Federated Timeline" -msgstr "Föderierte Zeitleiste" - -#: src/Views/HomeView.vala:12 src/Views/TimelineView.vala:36 +#: src/Views/Timeline.vala:34 src/Views/Home.vala:12 msgid "Home" msgstr "Startseite" -#: src/Views/LocalView.vala:12 +#: src/Views/Local.vala:12 msgid "Local Timeline" msgstr "Lokale Zeitleiste" -#: src/Views/SearchView.vala:82 +#: src/Views/Federated.vala:12 +msgid "Federated Timeline" +msgstr "Föderierte Zeitleiste" + +#: src/Views/Profile.vala:61 +#, c-format +msgid "%s Posts" +msgstr "" + +#: src/Views/Profile.vala:67 +#, fuzzy, c-format +msgid "%s Follows" +msgstr "Folgt" + +#: src/Views/Profile.vala:73 +#, fuzzy, c-format +msgid "%s Followers" +msgstr "Follower" + +#: src/Views/Profile.vala:109 +msgid "Sent follow request" +msgstr "Followanfrage gesendet" + +#: src/Views/Profile.vala:111 +#, fuzzy +msgid "Mutually follows you" +msgstr "Folgt dir" + +#: src/Views/Profile.vala:113 +msgid "Follows you" +msgstr "Folgt dir" + +#: src/Views/Profile.vala:124 +#, fuzzy +msgid "Follow back" +msgstr "Folgen" + +#: src/Views/Profile.vala:126 +msgid "Unfollow" +msgstr "Entfolgen" + +#: src/Views/Profile.vala:128 +msgid "Follow" +msgstr "Folgen" + +#: src/Views/Search.vala:82 msgid "Accounts" msgstr "Konten" -#: src/Views/SearchView.vala:91 +#: src/Views/Search.vala:91 msgid "Statuses" msgstr "Beiträge" +#~ msgid "TLS Error" +#~ msgstr "TLS-Fehler" + +#~ msgid "Can't ensure secure connection: " +#~ msgstr "Kann keine sichere Verbindung aufbauen: " + +#~ msgid "Error: %s" +#~ msgstr "Fehler: %s" + +#~ msgid "Boosted!" +#~ msgstr "Geteilt!" + +#~ msgid "Removed boost" +#~ msgstr "Boost entfernt" + +#~ msgid "Favorited!" +#~ msgstr "Favorisiert!" + +#~ msgid "Removed from favorites" +#~ msgstr "Von den Favoriten entfernt" + +#~ msgid "Muted!" +#~ msgstr "Stummgeschaltet!" + +#~ msgid "Conversation unmuted" +#~ msgstr "Konversation nicht mehr stummgeschaltet" + +#~ msgid "Pinned!" +#~ msgstr "Angepinnt!" + +#~ msgid "Unpinned from profile" +#~ msgstr "Vom Profil losgelöst" + +#~ msgid "Poof!" +#~ msgstr "Poof!" + +#~ msgid "New Account" +#~ msgstr "Neuer Account" + +#~ msgid "Click to add" +#~ msgstr "Klicken zum Hinzufügen" + +#~ msgid "Click to open %s media" +#~ msgstr "Klicken um %s Medien zu öffnen" + +#~ msgid "Uploading..." +#~ msgstr "Hochladen..." + +#~ msgid "File read error" +#~ msgstr "Dateilesefehler" + +#~ msgid "Can't read file %s: %s" +#~ msgstr "Kann Datei nicht lesen %s: %s" + +#~ msgid "Remove" +#~ msgstr "Entfernen" + +#~ msgid "Download" +#~ msgstr "Herunterladen" + +#~ msgid "Unknown Notification" +#~ msgstr "Unbekannte Benachrichtigung" + +#~ msgid "Dismiss" +#~ msgstr "Verwerfen" + +#~ msgid "Accept" +#~ msgstr "Aktzeptieren" + +#~ msgid "Reject" +#~ msgstr "Ablehnen" + +#~ msgid "Boost" +#~ msgstr "Teilen" + +#~ msgid "Favorite" +#~ msgstr "Favorisieren" + +#~ msgid "Reply" +#~ msgstr "Antworten" + +#~ msgid "[ This post contains sensitive content ]" +#~ msgstr "[ Dieser Beitrag beinhaltet sensibles Material ]" + +#~ msgid "Unmute Conversation" +#~ msgstr "Konversation nicht mehr stummschalten" + +#~ msgid "Mute Conversation" +#~ msgstr "Konversation stummschalten" + +#~ msgid "New Account" +#~ msgstr "Neues Konto" + +#~ msgid "What's an instance?" +#~ msgstr "Was ist eine Instanz?" + +#~ msgid "Code:" +#~ msgstr "Code:" + +#~ msgid "Paste your instance authorization code here" +#~ msgstr "Füge dein Autorisierungstoken hier ein" + +#~ msgid "Add Account" +#~ msgstr "Konto hinzufügen" + +#~ msgid "Instance:" +#~ msgstr "Instanz:" + +#~ msgid "Post Visibility" +#~ msgstr "Post-Sichtbarkeit" + +#~ msgid "Add Media" +#~ msgstr "Medien hinzufügen" + +#~ msgid "Spoiler Warning" +#~ msgstr "Inhaltswarnung" + +#~ msgid "Cancel" +#~ msgstr "Abbrechen" + +#~ msgid "Toot!" +#~ msgstr "Tröt!" + +#~ msgid "Write your warning here" +#~ msgstr "Schreibe deine Warnung hier" + +#~ msgid "Edit Profile" +#~ msgstr "Profil bearbeiten" + +#~ msgid "Mention" +#~ msgstr "Erwähnen" + +#~ msgid "Report" +#~ msgstr "Melden" + +#~ msgid "Mute" +#~ msgstr "Stummschalten" + +#~ msgid "Block" +#~ msgstr "Blockieren" + +#~ msgid "More Actions" +#~ msgstr "Weitere Aktionen" + +#~ msgid "Toots" +#~ msgstr "Beiträge" + +#~ msgid "Unblock" +#~ msgstr "Entsperren" + +#~ msgid "Unmute" +#~ msgstr "Nicht mehr stummschalten" + +#~ msgid "Blocked" +#~ msgstr "Blockiert" + +#~ msgid "Blocking this instance" +#~ msgstr "Diese Instanz blockieren" + +#~ msgid "User not found" +#~ msgstr "Benutzer nicht gefunden" + #~ msgid "Conversation muted" #~ msgstr "Konversation stummgeschaltet" diff --git a/po/fr_FR.po b/po/fr_FR.po index bfbc8ae..086e12e 100644 --- a/po/fr_FR.po +++ b/po/fr_FR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.bleakgrey.tootle\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-10-30 19:17+0300\n" +"POT-Creation-Date: 2019-09-16 16:00+0300\n" "PO-Revision-Date: 2018-06-17 10:07+0200\n" "Last-Translator: Guillaume\n" "Language-Team: none\n" @@ -20,7 +20,7 @@ msgstr "" "X-Poedit-SourceCharset: UTF-8\n" #: data/com.github.bleakgrey.tootle.desktop.in:4 -#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 src/MainWindow.vala:68 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 msgid "Tootle" msgstr "Tootle" @@ -42,30 +42,34 @@ msgid "Lightning fast client for Mastodon" msgstr "Client léger et rapide pour Mastodon" #: data/com.github.bleakgrey.tootle.appdata.xml.in:11 +#, fuzzy msgid "" "Tootle is a client for the world’s largest free, open-source, decentralized " -"microblogging network with real-time notifications and multiple accounts " -"support." +"microblogging network with real-time notifications and support for multiple " +"accounts." msgstr "" "Tootle est un client pour le plus grand réseau mondial de microblogging " "décentralisé, libre et open-source, avec un support multicomptes et la " "gestion des notifications instantanées." #: data/com.github.bleakgrey.tootle.appdata.xml.in:14 +#, fuzzy msgid "" -"Mastodon is lovely crafted with power and speed in mind, resulting in a " -"free, independent and popular alternative to the centralized social networks." +"Mastodon is lovingly crafted with power and speed in mind, resulting in a " +"free, independent, and popular alternative to the centralized social " +"networks." msgstr "" "Mastodon est conçu avec amour dans un objectif de puissance et de vitesse, " "en faisant une alternative libre, indépendante et populaire aux réseaux " "sociaux centralisés." #: data/com.github.bleakgrey.tootle.appdata.xml.in:17 +#, fuzzy msgid "" -"Anyone can run a server of Mastodon. Each server hosts individual user " -"accounts, the content they produce, and the content they are subscribed. " -"Every user can follow each other and share their posts regardless of their " -"server." +"Anyone can run a Mastodon server. Each server hosts individual user " +"accounts, the content they produce, and the content to which they are " +"subscribed. Every user can follow each other and share their posts " +"regardless of their server." msgstr "" "N'importe qui peut faire tourner un serveur Mastodon. Chaque serveur héberge " "des comptes utilisateurs individuels, le contenu qu'ils produisent et le " @@ -76,348 +80,282 @@ msgstr "" msgid "bleak_grey" msgstr "" -#: src/Desktop.vala:10 src/API/Account.vala:123 src/API/Account.vala:142 -#: src/API/Account.vala:161 src/Dialogs/NewAccountDialog.vala:102 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:80 +#, fuzzy +msgid "Added Watchlist" +msgstr "Liste de veille" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:81 +msgid "Added Redraft support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:82 +msgid "Added Pinning support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:83 +msgid "Added Simplified Chinese and German translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:84 +msgid "Added --hidden Start Flag" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:85 +msgid "Added Shortcuts and Back mouse button support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:86 +msgid "Changed Notifications screen behavior" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:87 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:102 +msgid "Fixed minor bugs" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:94 +msgid "Added Russian, French and Polish translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:95 +#, fuzzy +msgid "Added Direct timeline" +msgstr "Mettre à jour le fil public:" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:96 +msgid "Added support for custom character limit" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:97 +msgid "Added support for streaming all timelines" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:98 +msgid "Added tooltips for image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:99 +msgid "Added remove action for attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:100 +msgid "Changed behavior for mentioning users" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:101 +msgid "Changed behavior for missing image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:109 +msgid "Initial release" +msgstr "" + +#: src/Desktop.vala:10 msgid "Error" msgstr "Erreur" -#: src/Desktop.vala:46 +#: src/Desktop.vala:47 msgid "Media downloaded" msgstr "Médias téléchargés" -#: src/MainWindow.vala:48 -msgid "Back" -msgstr "Retour" - -#: src/MainWindow.vala:54 src/Dialogs/PostDialog.vala:29 -msgid "Toot" -msgstr "Écrire un message" - -#: src/Network.vala:58 -msgid "TLS Error" -msgstr "Erreur TLS" - -#: src/Network.vala:58 -msgid "Can't ensure secure connection: " -msgstr "Impossible d'assurer une connexion sécurisée:" - -#: src/Network.vala:66 +#: src/Services/Accounts.vala:31 #, c-format -msgid "Error: %s" -msgstr "Erreur: %s" +msgid "" +"This instance has invalidated this session. Please sign in again.\n" +"\n" +"%s" +msgstr "" -#: src/API/NotificationType.vala:50 -#, c-format -msgid "%s mentioned you" -msgstr "%s vous a mentionné" +#: src/Services/Network.vala:86 +msgid "Network Error" +msgstr "Erreur réseau" -#: src/API/NotificationType.vala:52 -#, c-format -msgid "%s boosted your toot" -msgstr "%s a partagé votre pouet" +#: src/API/Visibility.vala:36 +msgid "Unlisted" +msgstr "" -#: src/API/NotificationType.vala:54 -#, c-format -msgid "%s favorited your toot" -msgstr "%s a ajouté votre pouet à ses favoris" - -#: src/API/NotificationType.vala:56 -#, c-format -msgid "%s now follows you" -msgstr "%s vous suit désormais" - -#: src/API/NotificationType.vala:58 -#, c-format -msgid "%s wants to follow you" -msgstr "%s demande à vous suivre" - -#: src/API/NotificationType.vala:60 -#, fuzzy, c-format -msgid "%s posted a toot" -msgstr "%s a publié un pouet" - -#: src/API/Status.vala:174 -msgid "Boosted!" -msgstr "Partagé!" - -#: src/API/Status.vala:176 -msgid "Removed boost" -msgstr "Partage annulé" - -#: src/API/Status.vala:189 -msgid "Favorited!" -msgstr "Ajouté aux favoris!" - -#: src/API/Status.vala:191 -msgid "Removed from favorites" -msgstr "Supprimé des favoris" - -#: src/API/Status.vala:204 +#: src/API/Visibility.vala:38 #, fuzzy -msgid "Muted!" -msgstr "Masqué!" +msgid "Followers-only" +msgstr "Abonnés" -#: src/API/Status.vala:206 -msgid "Conversation unmuted" -msgstr "Discussion visible" +#: src/API/Visibility.vala:40 +msgid "Direct" +msgstr "" -#: src/API/Status.vala:219 -msgid "Pinned!" -msgstr "Épinglé!" +#: src/API/Visibility.vala:42 +msgid "Public" +msgstr "" -#: src/API/Status.vala:221 -#, fuzzy -msgid "Unpinned from profile" -msgstr "Désépinglé du profil" - -#: src/API/Status.vala:231 -msgid "Poof!" -msgstr "Poof!" - -#: src/API/StatusVisibility.vala:40 -msgid "Post to public timelines" -msgstr "Afficher dans le fil public" - -#: src/API/StatusVisibility.vala:42 +#: src/API/Visibility.vala:49 msgid "Don't post to public timelines" msgstr "Ne pas afficher dans le fil public" -#: src/API/StatusVisibility.vala:44 +#: src/API/Visibility.vala:51 msgid "Post to followers only" msgstr "Afficher seulement pour les abonnés" -#: src/API/StatusVisibility.vala:46 +#: src/API/Visibility.vala:53 msgid "Post to mentioned users only" msgstr "N'envoyer qu'aux personnes mentionnées" -#: src/Widgets/AccountsButton.vala:67 -msgid "Refresh" -msgstr "Actualiser" +#: src/API/Visibility.vala:55 +msgid "Post to public timelines" +msgstr "Afficher dans le fil public" -#: src/Widgets/AccountsButton.vala:71 -msgid "Favorites" -msgstr "Favoris" +#: src/API/NotificationType.vala:56 +#, fuzzy, c-format +msgid "" +"%s mentioned you" +msgstr "%s vous a mentionné" -#: src/Widgets/AccountsButton.vala:75 src/Views/DirectView.vala:12 -msgid "Direct Messages" -msgstr "Messages directs" +#: src/API/NotificationType.vala:58 +#, fuzzy, c-format +msgid "" +"%s boosted your status" +msgstr "%s a partagé votre pouet" -#: src/Widgets/AccountsButton.vala:79 src/Views/SearchView.vala:12 -msgid "Search" -msgstr "Chercher" +#: src/API/NotificationType.vala:60 +#, fuzzy, c-format +msgid "%s boosted" +msgstr "%s a partagé" -#: src/Widgets/AccountsButton.vala:83 -msgid "Watchlist" -msgstr "Liste de veille" +#: src/API/NotificationType.vala:62 +#, fuzzy, c-format +msgid "" +"%s favorited your status" +msgstr "%s a ajouté votre pouet à ses favoris" -#: src/Widgets/AccountsButton.vala:87 src/Dialogs/SettingsDialog.vala:18 -msgid "Settings" -msgstr "Paramètres" +#: src/API/NotificationType.vala:64 +#, fuzzy, c-format +msgid "" +"%s now follows you" +msgstr "%s vous suit désormais" -#: src/Widgets/AccountsButton.vala:142 -msgid "New Account" -msgstr "Nouveau Compte" +#: src/API/NotificationType.vala:66 +#, fuzzy, c-format +msgid "" +"%s wants to follow you" +msgstr "%s demande à vous suivre" -#: src/Widgets/AccountsButton.vala:143 -msgid "Click to add" -msgstr "Cliquer pour ajouter" +#: src/API/NotificationType.vala:68 +#, fuzzy, c-format +msgid "" +"%s posted a status" +msgstr "%s a publié un pouet" -#: src/Widgets/AccountWidget.vala:24 src/Widgets/AttachmentWidget.vala:130 -#: src/Widgets/StatusWidget.vala:289 +#: src/Widgets/RichLabel.vala:105 src/Widgets/Status.vala:206 +#: src/Widgets/Account.vala:28 msgid "Open in Browser" msgstr "Ouvrir dans le navigateur" -#: src/Widgets/AccountWidget.vala:26 src/Widgets/AttachmentWidget.vala:132 -#: src/Widgets/StatusWidget.vala:291 -msgid "Copy Link" -msgstr "Copier le lien" +#: src/Widgets/AccountsButton.vala:62 +msgid "Refresh" +msgstr "Actualiser" -#: src/Widgets/AttachmentBox.vala:41 -msgid "Select media files to add" -msgstr "Sélectionner les média à ajouter" +#: src/Widgets/AccountsButton.vala:67 +msgid "Favorites" +msgstr "Favoris" -#: src/Widgets/AttachmentBox.vala:44 -msgid "_Cancel" -msgstr "_Annuler" +#: src/Widgets/AccountsButton.vala:71 src/Views/Direct.vala:12 +msgid "Direct Messages" +msgstr "Messages directs" -#: src/Widgets/AttachmentBox.vala:46 -msgid "_Open" -msgstr "_Ouvrir" +#: src/Widgets/AccountsButton.vala:75 src/Views/Search.vala:12 +msgid "Search" +msgstr "Chercher" -#: src/Widgets/AttachmentWidget.vala:67 -#, c-format -msgid "Click to open %s media" -msgstr "Cliquer pour ouvrir les médias %s" +#: src/Widgets/AccountsButton.vala:79 src/Dialogs/WatchlistEditor.vala:99 +msgid "Watchlist" +msgstr "Liste de veille" -#: src/Widgets/AttachmentWidget.vala:84 -msgid "Uploading..." -msgstr "Téléchargement en cours..." +#: src/Widgets/AccountsButton.vala:83 src/Dialogs/Preferences.vala:17 +msgid "Settings" +msgstr "Paramètres" -#: src/Widgets/AttachmentWidget.vala:105 -msgid "File read error" -msgstr "Erreur de lecture du fichier" +#: src/Widgets/Status.vala:52 +msgid "[ Show more ]" +msgstr "" -#: src/Widgets/AttachmentWidget.vala:105 -#, c-format -msgid "Can't read file %s: %s" -msgstr "Impossible de lire le fichier %s: %s" - -#: src/Widgets/AttachmentWidget.vala:124 -msgid "Remove" -msgstr "Supprimer" - -#: src/Widgets/AttachmentWidget.vala:134 -msgid "Download" -msgstr "Télécharger" - -#: src/Widgets/NotificationWidget.vala:20 -msgid "Unknown Notification" -msgstr "Notification inconnue" - -#: src/Widgets/NotificationWidget.vala:25 -msgid "Dismiss" -msgstr "Annuler" - -#: src/Widgets/NotificationWidget.vala:64 -msgid "Accept" -msgstr "Accepter" - -#: src/Widgets/NotificationWidget.vala:66 -msgid "Reject" -msgstr "Rejeter" - -#: src/Widgets/StatusWidget.vala:84 -msgid "Boost" -msgstr "Partager" - -#: src/Widgets/StatusWidget.vala:91 -msgid "Favorite" -msgstr "Ajouter aux favoris" - -#: src/Widgets/StatusWidget.vala:98 -msgid "Reply" -msgstr "Répondre" - -#: src/Widgets/StatusWidget.vala:136 -#, c-format -msgid "%s boosted" -msgstr "%s a partagé" - -#: src/Widgets/StatusWidget.vala:151 -msgid "Toggle content" -msgstr "Afficher le contenu" - -#: src/Widgets/StatusWidget.vala:165 -msgid "[ This post contains sensitive content ]" -msgstr "[ Ce message comporte un contenu sensible ]" - -#: src/Widgets/StatusWidget.vala:234 +#: src/Widgets/Status.vala:120 msgid "This post can't be boosted" msgstr "Ce message ne peut être partagé" -#: src/Widgets/StatusWidget.vala:287 -msgid "Unmute Conversation" -msgstr "Rétablir la discussion" +#: src/Widgets/Status.vala:208 src/Widgets/Account.vala:30 +msgid "Copy Link" +msgstr "Copier le lien" -#: src/Widgets/StatusWidget.vala:287 -msgid "Mute Conversation" -msgstr "Masquer la discussion" - -#: src/Widgets/StatusWidget.vala:293 +#: src/Widgets/Status.vala:210 msgid "Copy Text" msgstr "Copier le texte" -#: src/Widgets/StatusWidget.vala:300 +#: src/Widgets/Status.vala:217 #, fuzzy msgid "Unpin from Profile" msgstr "Désépingler du profil" -#: src/Widgets/StatusWidget.vala:300 +#: src/Widgets/Status.vala:217 #, fuzzy msgid "Pin on Profile" msgstr "Épingler sur le profil" -#: src/Widgets/StatusWidget.vala:304 +#: src/Widgets/Status.vala:221 msgid "Delete" msgstr "Supprimer" -#: src/Widgets/StatusWidget.vala:308 src/Dialogs/PostDialog.vala:72 +#: src/Widgets/Status.vala:225 src/Dialogs/Compose.vala:73 msgid "Redraft" msgstr "Réecrire" -#: src/Dialogs/NewAccountDialog.vala:27 -msgid "New Account" -msgstr "Nouveau compte" +#: src/Widgets/Attachment/Box.vala:28 +msgid "Select media files to add" +msgstr "Sélectionner les média à ajouter" -#: src/Dialogs/NewAccountDialog.vala:38 -msgid "What's an instance?" -msgstr "Qu'est une instance?" +#: src/Widgets/Attachment/Box.vala:31 +msgid "_Cancel" +msgstr "_Annuler" -#: src/Dialogs/NewAccountDialog.vala:42 -msgid "Code:" -msgstr "Code:" +#: src/Widgets/Attachment/Box.vala:33 +msgid "_Open" +msgstr "_Ouvrir" -#: src/Dialogs/NewAccountDialog.vala:46 -msgid "Paste your instance authorization code here" -msgstr "Coller le code d'autorisation de votre instance ici" +#: src/Dialogs/MainWindow.vala:49 +msgid "Back" +msgstr "Retour" -#: src/Dialogs/NewAccountDialog.vala:49 -msgid "Add Account" -msgstr "Ajouter un compte" +#: src/Dialogs/MainWindow.vala:58 +msgid "Toot" +msgstr "Écrire un message" -#: src/Dialogs/NewAccountDialog.vala:60 -msgid "Instance:" -msgstr "Instance:" +#: src/Dialogs/MainWindow.vala:63 +msgid "Toggle content" +msgstr "Afficher le contenu" -#: src/Dialogs/NewAccountDialog.vala:102 -msgid "Please paste valid instance authorization code" -msgstr "Veuillez coller un code d'autorisation valide" +#: src/Dialogs/Compose.vala:69 +msgid "Post" +msgstr "" -#: src/Dialogs/NewAccountDialog.vala:110 -msgid "Network Error" -msgstr "Erreur réseau" - -#: src/Dialogs/PostDialog.vala:45 -msgid "Post Visibility" -msgstr "Visibilité du message" - -#: src/Dialogs/PostDialog.vala:52 -msgid "Add Media" -msgstr "Ajouter un média" - -#: src/Dialogs/PostDialog.vala:61 -msgid "Spoiler Warning" -msgstr "Ajouter un avertissement" - -#: src/Dialogs/PostDialog.vala:68 -msgid "Cancel" -msgstr "Annuler" - -#: src/Dialogs/PostDialog.vala:77 -msgid "Toot!" -msgstr "Envoyer le message!" - -#: src/Dialogs/PostDialog.vala:85 -msgid "Write your warning here" -msgstr "Écrire votre avertissement ici" - -#: src/Dialogs/SettingsDialog.vala:37 +#: src/Dialogs/Preferences.vala:36 msgid "Appearance" msgstr "Apparence" -#: src/Dialogs/SettingsDialog.vala:38 +#: src/Dialogs/Preferences.vala:37 msgid "Dark theme:" msgstr "Thème sombre:" -#: src/Dialogs/SettingsDialog.vala:41 +#: src/Dialogs/Preferences.vala:40 msgid "Timelines" msgstr "Fils" -#: src/Dialogs/SettingsDialog.vala:42 +#: src/Dialogs/Preferences.vala:41 msgid "Real-time updates:" msgstr "Mises à jour instantanées:" -#: src/Dialogs/SettingsDialog.vala:44 +#: src/Dialogs/Preferences.vala:43 msgid "Update public timelines:" msgstr "Mettre à jour le fil public:" @@ -425,144 +363,290 @@ msgstr "Mettre à jour le fil public:" #. grid.attach (new SettingsLabel (_("Use cache:")), 0, i); #. grid.attach (new SettingsSwitch ("cache"), 1, i++); #. grid.attach (new SettingsLabel (_("Max cache size (MB):")), 0, i); -#. var cache_size = new Gtk.SpinButton.with_range (16, 256, 1); +#. var cache_size = new SpinButton.with_range (16, 256, 1); #. settings.schema.bind ("cache-size", cache_size, "value", SettingsBindFlags.DEFAULT); #. grid.attach (cache_size, 1, i++); -#: src/Dialogs/SettingsDialog.vala:55 src/Views/NotificationsView.vala:34 +#: src/Dialogs/Preferences.vala:54 src/Views/Notifications.vala:33 msgid "Notifications" msgstr "Notifications" -#: src/Dialogs/SettingsDialog.vala:56 +#: src/Dialogs/Preferences.vala:55 msgid "Display notifications:" msgstr "Afficher les notifications:" -#: src/Dialogs/SettingsDialog.vala:58 +#: src/Dialogs/Preferences.vala:57 msgid "Always receive notifications:" msgstr "Toujours recevoir les notifications:" -#: src/Dialogs/SettingsDialog.vala:64 +#: src/Dialogs/Preferences.vala:63 src/Dialogs/WatchlistEditor.vala:162 msgid "_Close" msgstr "_Fermer" -#: src/Dialogs/WatchlistDialog.vala:20 +#: src/Dialogs/WatchlistEditor.vala:20 msgid "" "You'll be notified when toots from this user appear in your Home timeline." -msgstr "Vous serez averti lorsque des messages de cet utilisateur apparaîtront sur votre fil d'actualité." +msgstr "" +"Vous serez averti lorsque des messages de cet utilisateur apparaîtront sur " +"votre fil d'actualité." -#: src/Dialogs/WatchlistDialog.vala:21 +#: src/Dialogs/WatchlistEditor.vala:21 msgid "" "You'll be notified when toots with this hashtag appear in any public " "timelines." -msgstr "Vous serez averti lorsque des toots contenant ce hashtag apparaîtront dans n'importe quel fil public." +msgstr "" +"Vous serez averti lorsque des toots contenant ce hashtag apparaîtront dans " +"n'importe quel fil public." -#: src/Dialogs/WatchlistDialog.vala:137 +#: src/Dialogs/WatchlistEditor.vala:108 msgid "Users" msgstr "Utilisateurs" -#: src/Dialogs/WatchlistDialog.vala:138 src/Views/SearchView.vala:100 +#: src/Dialogs/WatchlistEditor.vala:109 src/Views/Search.vala:100 msgid "Hashtags" msgstr "Hashtags" -#: src/Dialogs/WatchlistDialog.vala:148 +#: src/Dialogs/WatchlistEditor.vala:122 msgid "Add" msgstr "Ajouter" -#: src/Views/AbstractView.vala:59 +#: src/Views/Base.vala:6 msgid "Nothing to see here" msgstr "Rien à voir par ici" -#: src/Views/AccountView.vala:79 -msgid "Edit Profile" -msgstr "Éditer votre profil" +#: src/Views/NewAccount.vala:91 +msgid "Instance URL is invalid" +msgstr "" -#: src/Views/AccountView.vala:80 -msgid "Mention" -msgstr "Mentionner" +#: src/Views/NewAccount.vala:133 +#, fuzzy +msgid "Please paste a valid authorization code" +msgstr "Veuillez coller un code d'autorisation valide" -#: src/Views/AccountView.vala:81 -msgid "Report" -msgstr "Signaler" - -#: src/Views/AccountView.vala:82 src/Views/AccountView.vala:167 -msgid "Mute" -msgstr "Masquer" - -#: src/Views/AccountView.vala:83 src/Views/AccountView.vala:166 -msgid "Block" -msgstr "Bloquer" - -#: src/Views/AccountView.vala:95 -msgid "More Actions" -msgstr "Plus d'actions" - -#: src/Views/AccountView.vala:115 -msgid "Toots" -msgstr "Pouets" - -#: src/Views/AccountView.vala:116 -msgid "Follows" -msgstr "Abonnements" - -#: src/Views/AccountView.vala:120 -msgid "Followers" -msgstr "Abonnés" - -#: src/Views/AccountView.vala:155 -msgid "Unfollow" -msgstr "Ne plus suivre" - -#: src/Views/AccountView.vala:159 -msgid "Follow" -msgstr "Suivre" - -#: src/Views/AccountView.vala:166 -msgid "Unblock" -msgstr "Débloquer" - -#: src/Views/AccountView.vala:167 -msgid "Unmute" -msgstr "Rétablir" - -#: src/Views/AccountView.vala:228 -msgid "Sent follow request" -msgstr "Demande de suivi envoyée" - -#: src/Views/AccountView.vala:230 -msgid "Blocked" -msgstr "Bloqué" - -#: src/Views/AccountView.vala:232 -msgid "Follows you" -msgstr "Vous suis actuellement" - -#: src/Views/AccountView.vala:234 -msgid "Blocking this instance" -msgstr "Blocage de cette instance" - -#: src/Views/AccountView.vala:269 -msgid "User not found" -msgstr "Utilisateur non trouvé" - -#: src/Views/FederatedView.vala:12 -msgid "Federated Timeline" -msgstr "Fil global" - -#: src/Views/HomeView.vala:12 src/Views/TimelineView.vala:36 +#: src/Views/Timeline.vala:34 src/Views/Home.vala:12 msgid "Home" msgstr "Accueil" -#: src/Views/LocalView.vala:12 +#: src/Views/Local.vala:12 msgid "Local Timeline" msgstr "Fil local" -#: src/Views/SearchView.vala:82 +#: src/Views/Federated.vala:12 +msgid "Federated Timeline" +msgstr "Fil global" + +#: src/Views/Profile.vala:61 +#, c-format +msgid "%s Posts" +msgstr "" + +#: src/Views/Profile.vala:67 +#, fuzzy, c-format +msgid "%s Follows" +msgstr "Abonnements" + +#: src/Views/Profile.vala:73 +#, fuzzy, c-format +msgid "%s Followers" +msgstr "Abonnés" + +#: src/Views/Profile.vala:109 +msgid "Sent follow request" +msgstr "Demande de suivi envoyée" + +#: src/Views/Profile.vala:111 +#, fuzzy +msgid "Mutually follows you" +msgstr "Vous suis actuellement" + +#: src/Views/Profile.vala:113 +msgid "Follows you" +msgstr "Vous suis actuellement" + +#: src/Views/Profile.vala:124 +#, fuzzy +msgid "Follow back" +msgstr "Suivre" + +#: src/Views/Profile.vala:126 +msgid "Unfollow" +msgstr "Ne plus suivre" + +#: src/Views/Profile.vala:128 +msgid "Follow" +msgstr "Suivre" + +#: src/Views/Search.vala:82 msgid "Accounts" msgstr "Comptes" -#: src/Views/SearchView.vala:91 +#: src/Views/Search.vala:91 msgid "Statuses" msgstr "Statuts" +#~ msgid "TLS Error" +#~ msgstr "Erreur TLS" + +#~ msgid "Can't ensure secure connection: " +#~ msgstr "Impossible d'assurer une connexion sécurisée:" + +#~ msgid "Error: %s" +#~ msgstr "Erreur: %s" + +#~ msgid "Boosted!" +#~ msgstr "Partagé!" + +#~ msgid "Removed boost" +#~ msgstr "Partage annulé" + +#~ msgid "Favorited!" +#~ msgstr "Ajouté aux favoris!" + +#~ msgid "Removed from favorites" +#~ msgstr "Supprimé des favoris" + +#, fuzzy +#~ msgid "Muted!" +#~ msgstr "Masqué!" + +#~ msgid "Conversation unmuted" +#~ msgstr "Discussion visible" + +#~ msgid "Pinned!" +#~ msgstr "Épinglé!" + +#, fuzzy +#~ msgid "Unpinned from profile" +#~ msgstr "Désépinglé du profil" + +#~ msgid "Poof!" +#~ msgstr "Poof!" + +#~ msgid "New Account" +#~ msgstr "Nouveau Compte" + +#~ msgid "Click to add" +#~ msgstr "Cliquer pour ajouter" + +#~ msgid "Click to open %s media" +#~ msgstr "Cliquer pour ouvrir les médias %s" + +#~ msgid "Uploading..." +#~ msgstr "Téléchargement en cours..." + +#~ msgid "File read error" +#~ msgstr "Erreur de lecture du fichier" + +#~ msgid "Can't read file %s: %s" +#~ msgstr "Impossible de lire le fichier %s: %s" + +#~ msgid "Remove" +#~ msgstr "Supprimer" + +#~ msgid "Download" +#~ msgstr "Télécharger" + +#~ msgid "Unknown Notification" +#~ msgstr "Notification inconnue" + +#~ msgid "Dismiss" +#~ msgstr "Annuler" + +#~ msgid "Accept" +#~ msgstr "Accepter" + +#~ msgid "Reject" +#~ msgstr "Rejeter" + +#~ msgid "Boost" +#~ msgstr "Partager" + +#~ msgid "Favorite" +#~ msgstr "Ajouter aux favoris" + +#~ msgid "Reply" +#~ msgstr "Répondre" + +#~ msgid "[ This post contains sensitive content ]" +#~ msgstr "[ Ce message comporte un contenu sensible ]" + +#~ msgid "Unmute Conversation" +#~ msgstr "Rétablir la discussion" + +#~ msgid "Mute Conversation" +#~ msgstr "Masquer la discussion" + +#~ msgid "New Account" +#~ msgstr "Nouveau compte" + +#~ msgid "What's an instance?" +#~ msgstr "Qu'est une instance?" + +#~ msgid "Code:" +#~ msgstr "Code:" + +#~ msgid "Paste your instance authorization code here" +#~ msgstr "Coller le code d'autorisation de votre instance ici" + +#~ msgid "Add Account" +#~ msgstr "Ajouter un compte" + +#~ msgid "Instance:" +#~ msgstr "Instance:" + +#~ msgid "Post Visibility" +#~ msgstr "Visibilité du message" + +#~ msgid "Add Media" +#~ msgstr "Ajouter un média" + +#~ msgid "Spoiler Warning" +#~ msgstr "Ajouter un avertissement" + +#~ msgid "Cancel" +#~ msgstr "Annuler" + +#~ msgid "Toot!" +#~ msgstr "Envoyer le message!" + +#~ msgid "Write your warning here" +#~ msgstr "Écrire votre avertissement ici" + +#~ msgid "Edit Profile" +#~ msgstr "Éditer votre profil" + +#~ msgid "Mention" +#~ msgstr "Mentionner" + +#~ msgid "Report" +#~ msgstr "Signaler" + +#~ msgid "Mute" +#~ msgstr "Masquer" + +#~ msgid "Block" +#~ msgstr "Bloquer" + +#~ msgid "More Actions" +#~ msgstr "Plus d'actions" + +#~ msgid "Toots" +#~ msgstr "Pouets" + +#~ msgid "Unblock" +#~ msgstr "Débloquer" + +#~ msgid "Unmute" +#~ msgstr "Rétablir" + +#~ msgid "Blocked" +#~ msgstr "Bloqué" + +#~ msgid "Blocking this instance" +#~ msgstr "Blocage de cette instance" + +#~ msgid "User not found" +#~ msgstr "Utilisateur non trouvé" + #~ msgid "Conversation muted" #~ msgstr "Discussion masquée" diff --git a/po/pl.po b/po/pl.po index c939ce2..bed8787 100644 --- a/po/pl.po +++ b/po/pl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.bleakgrey.tootle\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-10-30 19:17+0300\n" +"POT-Creation-Date: 2019-09-16 16:00+0300\n" "PO-Revision-Date: 2018-11-01 12:20+0100\n" "Last-Translator: Marcin Mikołajczak \n" "Language-Team: Polish\n" @@ -18,7 +18,7 @@ msgstr "" "X-Generator: Poedit 2.1.1\n" #: data/com.github.bleakgrey.tootle.desktop.in:4 -#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 src/MainWindow.vala:68 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 msgid "Tootle" msgstr "Tootle" @@ -40,30 +40,34 @@ msgid "Lightning fast client for Mastodon" msgstr "Klient Mastodona szybki jak błyskawica" #: data/com.github.bleakgrey.tootle.appdata.xml.in:11 +#, fuzzy msgid "" "Tootle is a client for the world’s largest free, open-source, decentralized " -"microblogging network with real-time notifications and multiple accounts " -"support." +"microblogging network with real-time notifications and support for multiple " +"accounts." msgstr "" "Tootle jest klientem największej na świecie wolnej i otwartoźródłowej, " "zdecentralizowanej sieci społecznościowej obsługującym powiadomienia w " "czasie rzeczywistym i wiele kont jednocześnie." #: data/com.github.bleakgrey.tootle.appdata.xml.in:14 +#, fuzzy msgid "" -"Mastodon is lovely crafted with power and speed in mind, resulting in a " -"free, independent and popular alternative to the centralized social networks." +"Mastodon is lovingly crafted with power and speed in mind, resulting in a " +"free, independent, and popular alternative to the centralized social " +"networks." msgstr "" "Mastodon został zaprojektowany z myślą o funkcjonalności i szybkości, w " "wyniku czego powstałą wolna, niezależna i popularna alternatywa dla " "scentralizowanych sieci społecznościowych." #: data/com.github.bleakgrey.tootle.appdata.xml.in:17 +#, fuzzy msgid "" -"Anyone can run a server of Mastodon. Each server hosts individual user " -"accounts, the content they produce, and the content they are subscribed. " -"Every user can follow each other and share their posts regardless of their " -"server." +"Anyone can run a Mastodon server. Each server hosts individual user " +"accounts, the content they produce, and the content to which they are " +"subscribed. Every user can follow each other and share their posts " +"regardless of their server." msgstr "" "Każdy może uruchomić serwer Mastodona. Każdy serwer przechowuje konta innych " "użytkowników, zawartość przez nich tworzoną i wpisy obserwowanych osób. " @@ -74,344 +78,280 @@ msgstr "" msgid "bleak_grey" msgstr "bleak_grey" -#: src/Desktop.vala:10 src/API/Account.vala:123 src/API/Account.vala:142 -#: src/API/Account.vala:161 src/Dialogs/NewAccountDialog.vala:102 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:80 +#, fuzzy +msgid "Added Watchlist" +msgstr "Lista obserwowanych" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:81 +msgid "Added Redraft support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:82 +msgid "Added Pinning support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:83 +msgid "Added Simplified Chinese and German translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:84 +msgid "Added --hidden Start Flag" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:85 +msgid "Added Shortcuts and Back mouse button support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:86 +msgid "Changed Notifications screen behavior" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:87 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:102 +msgid "Fixed minor bugs" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:94 +msgid "Added Russian, French and Polish translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:95 +#, fuzzy +msgid "Added Direct timeline" +msgstr "Aktualizuj publiczne osie czasu:" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:96 +msgid "Added support for custom character limit" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:97 +msgid "Added support for streaming all timelines" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:98 +msgid "Added tooltips for image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:99 +msgid "Added remove action for attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:100 +msgid "Changed behavior for mentioning users" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:101 +msgid "Changed behavior for missing image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:109 +msgid "Initial release" +msgstr "" + +#: src/Desktop.vala:10 msgid "Error" msgstr "Błąd" -#: src/Desktop.vala:46 +#: src/Desktop.vala:47 msgid "Media downloaded" msgstr "Pobrano zawartość multimedialną" -#: src/MainWindow.vala:48 -msgid "Back" -msgstr "Wróć" - -#: src/MainWindow.vala:54 src/Dialogs/PostDialog.vala:29 -msgid "Toot" -msgstr "Wyślij" - -#: src/Network.vala:58 -msgid "TLS Error" -msgstr "Błąd TLS" - -#: src/Network.vala:58 -msgid "Can't ensure secure connection: " -msgstr "Nie można zapewnić bezpiecznego połączenia: " - -#: src/Network.vala:66 +#: src/Services/Accounts.vala:31 #, c-format -msgid "Error: %s" -msgstr "Błąd: %s" - -#: src/API/NotificationType.vala:50 -#, c-format -msgid "%s mentioned you" -msgstr "%s wspomniał o Tobie" - -#: src/API/NotificationType.vala:52 -#, c-format -msgid "%s boosted your toot" -msgstr "%s podbił Twój wpis" - -#: src/API/NotificationType.vala:54 -#, c-format -msgid "%s favorited your toot" -msgstr "%s dodał Twój wpis do ulubionych" - -#: src/API/NotificationType.vala:56 -#, c-format -msgid "%s now follows you" -msgstr "%s zaczął Cię śledzić" - -#: src/API/NotificationType.vala:58 -#, c-format -msgid "%s wants to follow you" -msgstr "%s chce Cię śledzić" - -#: src/API/NotificationType.vala:60 -#, c-format -msgid "%s posted a toot" -msgstr "%s utworzył wpis" - -#: src/API/Status.vala:174 -msgid "Boosted!" -msgstr "Podbito!" - -#: src/API/Status.vala:176 -msgid "Removed boost" -msgstr "Cofnięto podbicie" - -#: src/API/Status.vala:189 -msgid "Favorited!" -msgstr "Dodano do ulubionych!" - -#: src/API/Status.vala:191 -msgid "Removed from favorites" -msgstr "Usunięto z ulubionych" - -#: src/API/Status.vala:204 -msgid "Muted!" -msgstr "Wyciszono!" - -#: src/API/Status.vala:206 -msgid "Conversation unmuted" -msgstr "Cofnięto wyciszenie konwersacji" - -#: src/API/Status.vala:219 -msgid "Pinned!" -msgstr "Przypięto!" - -#: src/API/Status.vala:221 -msgid "Unpinned from profile" -msgstr "Cofnięto przypięcie" - -#: src/API/Status.vala:231 -msgid "Poof!" +msgid "" +"This instance has invalidated this session. Please sign in again.\n" +"\n" +"%s" msgstr "" -#: src/API/StatusVisibility.vala:40 -msgid "Post to public timelines" -msgstr "Wyświetlaj na publicznej osi czasu" - -#: src/API/StatusVisibility.vala:42 -msgid "Don't post to public timelines" -msgstr "Nie wyświetlaj na publicznej osi czasu" - -#: src/API/StatusVisibility.vala:44 -msgid "Post to followers only" -msgstr "Wyślij tylko dla śledzących" - -#: src/API/StatusVisibility.vala:46 -msgid "Post to mentioned users only" -msgstr "Wyślij tylko dla wspomnianych" - -#: src/Widgets/AccountsButton.vala:67 -msgid "Refresh" -msgstr "Odśwież" - -#: src/Widgets/AccountsButton.vala:71 -msgid "Favorites" -msgstr "Ulubione" - -#: src/Widgets/AccountsButton.vala:75 src/Views/DirectView.vala:12 -msgid "Direct Messages" -msgstr "Wiadomości bezpośrednie" - -#: src/Widgets/AccountsButton.vala:79 src/Views/SearchView.vala:12 -msgid "Search" -msgstr "Wyszukiwanie" - -#: src/Widgets/AccountsButton.vala:83 -msgid "Watchlist" -msgstr "Lista obserwowanych" - -#: src/Widgets/AccountsButton.vala:87 src/Dialogs/SettingsDialog.vala:18 -msgid "Settings" -msgstr "Ustawienia" - -#: src/Widgets/AccountsButton.vala:142 -msgid "New Account" -msgstr "Nowe konto" - -#: src/Widgets/AccountsButton.vala:143 -msgid "Click to add" -msgstr "Naciśnij aby dodać" - -#: src/Widgets/AccountWidget.vala:24 src/Widgets/AttachmentWidget.vala:130 -#: src/Widgets/StatusWidget.vala:289 -msgid "Open in Browser" -msgstr "Otwórz w przeglądarce" - -#: src/Widgets/AccountWidget.vala:26 src/Widgets/AttachmentWidget.vala:132 -#: src/Widgets/StatusWidget.vala:291 -msgid "Copy Link" -msgstr "Skopiuj odnośnik" - -#: src/Widgets/AttachmentBox.vala:41 -msgid "Select media files to add" -msgstr "Zaznacz pliki multimedialne do dodania" - -#: src/Widgets/AttachmentBox.vala:44 -msgid "_Cancel" -msgstr "_Anuluj" - -#: src/Widgets/AttachmentBox.vala:46 -msgid "_Open" -msgstr "_Otwórz" - -#: src/Widgets/AttachmentWidget.vala:67 -#, c-format -msgid "Click to open %s media" -msgstr "Naciśnij aby otworzyć %s multimediów" - -#: src/Widgets/AttachmentWidget.vala:84 -msgid "Uploading..." -msgstr "Wysyłanie…" - -#: src/Widgets/AttachmentWidget.vala:105 -msgid "File read error" -msgstr "Błąd odczytywania plików" - -#: src/Widgets/AttachmentWidget.vala:105 -#, c-format -msgid "Can't read file %s: %s" -msgstr "Nie można odczytać pliku %s: %s" - -#: src/Widgets/AttachmentWidget.vala:124 -msgid "Remove" -msgstr "Usuń" - -#: src/Widgets/AttachmentWidget.vala:134 -msgid "Download" -msgstr "Pobierz" - -#: src/Widgets/NotificationWidget.vala:20 -msgid "Unknown Notification" -msgstr "Nieznane powiadomienie" - -#: src/Widgets/NotificationWidget.vala:25 -msgid "Dismiss" -msgstr "Pomiń" - -#: src/Widgets/NotificationWidget.vala:64 -msgid "Accept" -msgstr "Zaakceptuj" - -#: src/Widgets/NotificationWidget.vala:66 -msgid "Reject" -msgstr "Odrzuć" - -#: src/Widgets/StatusWidget.vala:84 -msgid "Boost" -msgstr "Podbij" - -#: src/Widgets/StatusWidget.vala:91 -msgid "Favorite" -msgstr "Dodaj do ulubionych" - -#: src/Widgets/StatusWidget.vala:98 -msgid "Reply" -msgstr "Odpowiedz" - -#: src/Widgets/StatusWidget.vala:136 -#, c-format -msgid "%s boosted" -msgstr "%s podbił" - -#: src/Widgets/StatusWidget.vala:151 -msgid "Toggle content" -msgstr "Przełącz zawartość" - -#: src/Widgets/StatusWidget.vala:165 -msgid "[ This post contains sensitive content ]" -msgstr "[ Ten wpis zawiera zawartość wrażliwą ]" - -#: src/Widgets/StatusWidget.vala:234 -msgid "This post can't be boosted" -msgstr "Ten wpis nie może zostać podbity" - -#: src/Widgets/StatusWidget.vala:287 -msgid "Unmute Conversation" -msgstr "Cofnij wyciszenie konwersacji" - -#: src/Widgets/StatusWidget.vala:287 -msgid "Mute Conversation" -msgstr "Wycisz konwersację" - -#: src/Widgets/StatusWidget.vala:293 -msgid "Copy Text" -msgstr "Skopiuj tekst" - -#: src/Widgets/StatusWidget.vala:300 -msgid "Unpin from Profile" -msgstr "Cofnij przypięcie" - -#: src/Widgets/StatusWidget.vala:300 -msgid "Pin on Profile" -msgstr "Przypnij do profilu" - -#: src/Widgets/StatusWidget.vala:304 -msgid "Delete" -msgstr "Usuń" - -#: src/Widgets/StatusWidget.vala:308 src/Dialogs/PostDialog.vala:72 -msgid "Redraft" -msgstr "Przeredaguj" - -#: src/Dialogs/NewAccountDialog.vala:27 -msgid "New Account" -msgstr "Nowe konto" - -#: src/Dialogs/NewAccountDialog.vala:38 -msgid "What's an instance?" -msgstr "Czym jest instancja?" - -#: src/Dialogs/NewAccountDialog.vala:42 -msgid "Code:" -msgstr "Kod:" - -#: src/Dialogs/NewAccountDialog.vala:46 -msgid "Paste your instance authorization code here" -msgstr "Wklej kod autoryzacji swojej instancji tutaj" - -#: src/Dialogs/NewAccountDialog.vala:49 -msgid "Add Account" -msgstr "Dodaj konto" - -#: src/Dialogs/NewAccountDialog.vala:60 -msgid "Instance:" -msgstr "Instancja:" - -#: src/Dialogs/NewAccountDialog.vala:102 -msgid "Please paste valid instance authorization code" -msgstr "Wklej prawidłowy kod autoryzacji tutaj" - -#: src/Dialogs/NewAccountDialog.vala:110 +#: src/Services/Network.vala:86 msgid "Network Error" msgstr "Błąd sieci" -#: src/Dialogs/PostDialog.vala:45 -msgid "Post Visibility" -msgstr "Widoczność wpisu" +#: src/API/Visibility.vala:36 +msgid "Unlisted" +msgstr "" -#: src/Dialogs/PostDialog.vala:52 -msgid "Add Media" -msgstr "Dodaj zawartość multimedialną" +#: src/API/Visibility.vala:38 +#, fuzzy +msgid "Followers-only" +msgstr "Śledzący" -#: src/Dialogs/PostDialog.vala:61 -msgid "Spoiler Warning" -msgstr "Ostrzeżenie o zawartości" +#: src/API/Visibility.vala:40 +msgid "Direct" +msgstr "" -#: src/Dialogs/PostDialog.vala:68 -msgid "Cancel" -msgstr "Anuluj" +#: src/API/Visibility.vala:42 +msgid "Public" +msgstr "" -#: src/Dialogs/PostDialog.vala:77 -msgid "Toot!" +#: src/API/Visibility.vala:49 +msgid "Don't post to public timelines" +msgstr "Nie wyświetlaj na publicznej osi czasu" + +#: src/API/Visibility.vala:51 +msgid "Post to followers only" +msgstr "Wyślij tylko dla śledzących" + +#: src/API/Visibility.vala:53 +msgid "Post to mentioned users only" +msgstr "Wyślij tylko dla wspomnianych" + +#: src/API/Visibility.vala:55 +msgid "Post to public timelines" +msgstr "Wyświetlaj na publicznej osi czasu" + +#: src/API/NotificationType.vala:56 +#, fuzzy, c-format +msgid "" +"%s mentioned you" +msgstr "%s wspomniał o Tobie" + +#: src/API/NotificationType.vala:58 +#, fuzzy, c-format +msgid "" +"%s boosted your status" +msgstr "%s podbił Twój wpis" + +#: src/API/NotificationType.vala:60 +#, fuzzy, c-format +msgid "%s boosted" +msgstr "%s podbił" + +#: src/API/NotificationType.vala:62 +#, fuzzy, c-format +msgid "" +"%s favorited your status" +msgstr "%s dodał Twój wpis do ulubionych" + +#: src/API/NotificationType.vala:64 +#, fuzzy, c-format +msgid "" +"%s now follows you" +msgstr "%s zaczął Cię śledzić" + +#: src/API/NotificationType.vala:66 +#, fuzzy, c-format +msgid "" +"%s wants to follow you" +msgstr "%s chce Cię śledzić" + +#: src/API/NotificationType.vala:68 +#, fuzzy, c-format +msgid "" +"%s posted a status" +msgstr "%s utworzył wpis" + +#: src/Widgets/RichLabel.vala:105 src/Widgets/Status.vala:206 +#: src/Widgets/Account.vala:28 +msgid "Open in Browser" +msgstr "Otwórz w przeglądarce" + +#: src/Widgets/AccountsButton.vala:62 +msgid "Refresh" +msgstr "Odśwież" + +#: src/Widgets/AccountsButton.vala:67 +msgid "Favorites" +msgstr "Ulubione" + +#: src/Widgets/AccountsButton.vala:71 src/Views/Direct.vala:12 +msgid "Direct Messages" +msgstr "Wiadomości bezpośrednie" + +#: src/Widgets/AccountsButton.vala:75 src/Views/Search.vala:12 +msgid "Search" +msgstr "Wyszukiwanie" + +#: src/Widgets/AccountsButton.vala:79 src/Dialogs/WatchlistEditor.vala:99 +msgid "Watchlist" +msgstr "Lista obserwowanych" + +#: src/Widgets/AccountsButton.vala:83 src/Dialogs/Preferences.vala:17 +msgid "Settings" +msgstr "Ustawienia" + +#: src/Widgets/Status.vala:52 +msgid "[ Show more ]" +msgstr "" + +#: src/Widgets/Status.vala:120 +msgid "This post can't be boosted" +msgstr "Ten wpis nie może zostać podbity" + +#: src/Widgets/Status.vala:208 src/Widgets/Account.vala:30 +msgid "Copy Link" +msgstr "Skopiuj odnośnik" + +#: src/Widgets/Status.vala:210 +msgid "Copy Text" +msgstr "Skopiuj tekst" + +#: src/Widgets/Status.vala:217 +msgid "Unpin from Profile" +msgstr "Cofnij przypięcie" + +#: src/Widgets/Status.vala:217 +msgid "Pin on Profile" +msgstr "Przypnij do profilu" + +#: src/Widgets/Status.vala:221 +msgid "Delete" +msgstr "Usuń" + +#: src/Widgets/Status.vala:225 src/Dialogs/Compose.vala:73 +msgid "Redraft" +msgstr "Przeredaguj" + +#: src/Widgets/Attachment/Box.vala:28 +msgid "Select media files to add" +msgstr "Zaznacz pliki multimedialne do dodania" + +#: src/Widgets/Attachment/Box.vala:31 +msgid "_Cancel" +msgstr "_Anuluj" + +#: src/Widgets/Attachment/Box.vala:33 +msgid "_Open" +msgstr "_Otwórz" + +#: src/Dialogs/MainWindow.vala:49 +msgid "Back" +msgstr "Wróć" + +#: src/Dialogs/MainWindow.vala:58 +msgid "Toot" msgstr "Wyślij" -#: src/Dialogs/PostDialog.vala:85 -msgid "Write your warning here" -msgstr "Wprowadź zawartość ostrzeżenia tutaj" +#: src/Dialogs/MainWindow.vala:63 +msgid "Toggle content" +msgstr "Przełącz zawartość" -#: src/Dialogs/SettingsDialog.vala:37 +#: src/Dialogs/Compose.vala:69 +msgid "Post" +msgstr "" + +#: src/Dialogs/Preferences.vala:36 msgid "Appearance" msgstr "Wygląd" -#: src/Dialogs/SettingsDialog.vala:38 +#: src/Dialogs/Preferences.vala:37 msgid "Dark theme:" msgstr "Ciemny motyw:" -#: src/Dialogs/SettingsDialog.vala:41 +#: src/Dialogs/Preferences.vala:40 msgid "Timelines" msgstr "Osie czasu" -#: src/Dialogs/SettingsDialog.vala:42 +#: src/Dialogs/Preferences.vala:41 msgid "Real-time updates:" msgstr "Aktualizacje w czasie rzeczywistym:" -#: src/Dialogs/SettingsDialog.vala:44 +#: src/Dialogs/Preferences.vala:43 msgid "Update public timelines:" msgstr "Aktualizuj publiczne osie czasu:" @@ -419,33 +359,33 @@ msgstr "Aktualizuj publiczne osie czasu:" #. grid.attach (new SettingsLabel (_("Use cache:")), 0, i); #. grid.attach (new SettingsSwitch ("cache"), 1, i++); #. grid.attach (new SettingsLabel (_("Max cache size (MB):")), 0, i); -#. var cache_size = new Gtk.SpinButton.with_range (16, 256, 1); +#. var cache_size = new SpinButton.with_range (16, 256, 1); #. settings.schema.bind ("cache-size", cache_size, "value", SettingsBindFlags.DEFAULT); #. grid.attach (cache_size, 1, i++); -#: src/Dialogs/SettingsDialog.vala:55 src/Views/NotificationsView.vala:34 +#: src/Dialogs/Preferences.vala:54 src/Views/Notifications.vala:33 msgid "Notifications" msgstr "Powiadomienia" -#: src/Dialogs/SettingsDialog.vala:56 +#: src/Dialogs/Preferences.vala:55 msgid "Display notifications:" msgstr "Wyświetlaj powiadomienia:" -#: src/Dialogs/SettingsDialog.vala:58 +#: src/Dialogs/Preferences.vala:57 msgid "Always receive notifications:" msgstr "Zawsze otrzymuj powiadomienia:" -#: src/Dialogs/SettingsDialog.vala:64 +#: src/Dialogs/Preferences.vala:63 src/Dialogs/WatchlistEditor.vala:162 msgid "_Close" msgstr "_Zamknij" -#: src/Dialogs/WatchlistDialog.vala:20 +#: src/Dialogs/WatchlistEditor.vala:20 msgid "" "You'll be notified when toots from this user appear in your Home timeline." msgstr "" "Otrzymasz powiadomienie, kiedy wpisy tego użytkownika pojawią się na Twojej " "osi czasu." -#: src/Dialogs/WatchlistDialog.vala:21 +#: src/Dialogs/WatchlistEditor.vala:21 msgid "" "You'll be notified when toots with this hashtag appear in any public " "timelines." @@ -453,114 +393,251 @@ msgstr "" "Otrzymasz powiadomienie, kiedy wpisy z tym hashtagiem pojawią się na " "publicznej osi czasu." -#: src/Dialogs/WatchlistDialog.vala:137 +#: src/Dialogs/WatchlistEditor.vala:108 msgid "Users" msgstr "Użytkownicy" -#: src/Dialogs/WatchlistDialog.vala:138 src/Views/SearchView.vala:100 +#: src/Dialogs/WatchlistEditor.vala:109 src/Views/Search.vala:100 msgid "Hashtags" msgstr "Hashtagi" -#: src/Dialogs/WatchlistDialog.vala:148 +#: src/Dialogs/WatchlistEditor.vala:122 msgid "Add" msgstr "Dodaj" -#: src/Views/AbstractView.vala:59 +#: src/Views/Base.vala:6 msgid "Nothing to see here" msgstr "Nie ma nic do wyświetlenia" -#: src/Views/AccountView.vala:79 -msgid "Edit Profile" -msgstr "Edytuj profil" +#: src/Views/NewAccount.vala:91 +msgid "Instance URL is invalid" +msgstr "" -#: src/Views/AccountView.vala:80 -msgid "Mention" -msgstr "Wspomnij" +#: src/Views/NewAccount.vala:133 +#, fuzzy +msgid "Please paste a valid authorization code" +msgstr "Wklej prawidłowy kod autoryzacji tutaj" -#: src/Views/AccountView.vala:81 -msgid "Report" -msgstr "Zgłoś" - -#: src/Views/AccountView.vala:82 src/Views/AccountView.vala:167 -msgid "Mute" -msgstr "Wycisz" - -#: src/Views/AccountView.vala:83 src/Views/AccountView.vala:166 -msgid "Block" -msgstr "Zablokuj" - -#: src/Views/AccountView.vala:95 -msgid "More Actions" -msgstr "Więcej działań" - -#: src/Views/AccountView.vala:115 -msgid "Toots" -msgstr "Wpisy" - -#: src/Views/AccountView.vala:116 -msgid "Follows" -msgstr "Śledzeni" - -#: src/Views/AccountView.vala:120 -msgid "Followers" -msgstr "Śledzący" - -#: src/Views/AccountView.vala:155 -msgid "Unfollow" -msgstr "Przestań śledzić" - -#: src/Views/AccountView.vala:159 -msgid "Follow" -msgstr "Śledź" - -#: src/Views/AccountView.vala:166 -msgid "Unblock" -msgstr "Odblokuj" - -#: src/Views/AccountView.vala:167 -msgid "Unmute" -msgstr "Cofnij wyciszenie" - -#: src/Views/AccountView.vala:228 -msgid "Sent follow request" -msgstr "Wysłano prośbę o możliwość śledzenia" - -#: src/Views/AccountView.vala:230 -msgid "Blocked" -msgstr "Zablokowano" - -#: src/Views/AccountView.vala:232 -msgid "Follows you" -msgstr "Aledzi Cię" - -#: src/Views/AccountView.vala:234 -msgid "Blocking this instance" -msgstr "Blokujesz tę instancję" - -#: src/Views/AccountView.vala:269 -msgid "User not found" -msgstr "Nie znaleziono użytkownika" - -#: src/Views/FederatedView.vala:12 -msgid "Federated Timeline" -msgstr "Oś czasu federacji" - -#: src/Views/HomeView.vala:12 src/Views/TimelineView.vala:36 +#: src/Views/Timeline.vala:34 src/Views/Home.vala:12 msgid "Home" msgstr "Strona główna" -#: src/Views/LocalView.vala:12 +#: src/Views/Local.vala:12 msgid "Local Timeline" msgstr "Lokalna oś czasu" -#: src/Views/SearchView.vala:82 +#: src/Views/Federated.vala:12 +msgid "Federated Timeline" +msgstr "Oś czasu federacji" + +#: src/Views/Profile.vala:61 +#, c-format +msgid "%s Posts" +msgstr "" + +#: src/Views/Profile.vala:67 +#, fuzzy, c-format +msgid "%s Follows" +msgstr "Śledzeni" + +#: src/Views/Profile.vala:73 +#, fuzzy, c-format +msgid "%s Followers" +msgstr "Śledzący" + +#: src/Views/Profile.vala:109 +msgid "Sent follow request" +msgstr "Wysłano prośbę o możliwość śledzenia" + +#: src/Views/Profile.vala:111 +#, fuzzy +msgid "Mutually follows you" +msgstr "Aledzi Cię" + +#: src/Views/Profile.vala:113 +msgid "Follows you" +msgstr "Aledzi Cię" + +#: src/Views/Profile.vala:124 +#, fuzzy +msgid "Follow back" +msgstr "Śledź" + +#: src/Views/Profile.vala:126 +msgid "Unfollow" +msgstr "Przestań śledzić" + +#: src/Views/Profile.vala:128 +msgid "Follow" +msgstr "Śledź" + +#: src/Views/Search.vala:82 msgid "Accounts" msgstr "Kinda" -#: src/Views/SearchView.vala:91 +#: src/Views/Search.vala:91 msgid "Statuses" msgstr "Wpisy" +#~ msgid "TLS Error" +#~ msgstr "Błąd TLS" + +#~ msgid "Can't ensure secure connection: " +#~ msgstr "Nie można zapewnić bezpiecznego połączenia: " + +#~ msgid "Error: %s" +#~ msgstr "Błąd: %s" + +#~ msgid "Boosted!" +#~ msgstr "Podbito!" + +#~ msgid "Removed boost" +#~ msgstr "Cofnięto podbicie" + +#~ msgid "Favorited!" +#~ msgstr "Dodano do ulubionych!" + +#~ msgid "Removed from favorites" +#~ msgstr "Usunięto z ulubionych" + +#~ msgid "Muted!" +#~ msgstr "Wyciszono!" + +#~ msgid "Conversation unmuted" +#~ msgstr "Cofnięto wyciszenie konwersacji" + +#~ msgid "Pinned!" +#~ msgstr "Przypięto!" + +#~ msgid "Unpinned from profile" +#~ msgstr "Cofnięto przypięcie" + +#~ msgid "New Account" +#~ msgstr "Nowe konto" + +#~ msgid "Click to add" +#~ msgstr "Naciśnij aby dodać" + +#~ msgid "Click to open %s media" +#~ msgstr "Naciśnij aby otworzyć %s multimediów" + +#~ msgid "Uploading..." +#~ msgstr "Wysyłanie…" + +#~ msgid "File read error" +#~ msgstr "Błąd odczytywania plików" + +#~ msgid "Can't read file %s: %s" +#~ msgstr "Nie można odczytać pliku %s: %s" + +#~ msgid "Remove" +#~ msgstr "Usuń" + +#~ msgid "Download" +#~ msgstr "Pobierz" + +#~ msgid "Unknown Notification" +#~ msgstr "Nieznane powiadomienie" + +#~ msgid "Dismiss" +#~ msgstr "Pomiń" + +#~ msgid "Accept" +#~ msgstr "Zaakceptuj" + +#~ msgid "Reject" +#~ msgstr "Odrzuć" + +#~ msgid "Boost" +#~ msgstr "Podbij" + +#~ msgid "Favorite" +#~ msgstr "Dodaj do ulubionych" + +#~ msgid "Reply" +#~ msgstr "Odpowiedz" + +#~ msgid "[ This post contains sensitive content ]" +#~ msgstr "[ Ten wpis zawiera zawartość wrażliwą ]" + +#~ msgid "Unmute Conversation" +#~ msgstr "Cofnij wyciszenie konwersacji" + +#~ msgid "Mute Conversation" +#~ msgstr "Wycisz konwersację" + +#~ msgid "New Account" +#~ msgstr "Nowe konto" + +#~ msgid "What's an instance?" +#~ msgstr "Czym jest instancja?" + +#~ msgid "Code:" +#~ msgstr "Kod:" + +#~ msgid "Paste your instance authorization code here" +#~ msgstr "Wklej kod autoryzacji swojej instancji tutaj" + +#~ msgid "Add Account" +#~ msgstr "Dodaj konto" + +#~ msgid "Instance:" +#~ msgstr "Instancja:" + +#~ msgid "Post Visibility" +#~ msgstr "Widoczność wpisu" + +#~ msgid "Add Media" +#~ msgstr "Dodaj zawartość multimedialną" + +#~ msgid "Spoiler Warning" +#~ msgstr "Ostrzeżenie o zawartości" + +#~ msgid "Cancel" +#~ msgstr "Anuluj" + +#~ msgid "Toot!" +#~ msgstr "Wyślij" + +#~ msgid "Write your warning here" +#~ msgstr "Wprowadź zawartość ostrzeżenia tutaj" + +#~ msgid "Edit Profile" +#~ msgstr "Edytuj profil" + +#~ msgid "Mention" +#~ msgstr "Wspomnij" + +#~ msgid "Report" +#~ msgstr "Zgłoś" + +#~ msgid "Mute" +#~ msgstr "Wycisz" + +#~ msgid "Block" +#~ msgstr "Zablokuj" + +#~ msgid "More Actions" +#~ msgstr "Więcej działań" + +#~ msgid "Toots" +#~ msgstr "Wpisy" + +#~ msgid "Unblock" +#~ msgstr "Odblokuj" + +#~ msgid "Unmute" +#~ msgstr "Cofnij wyciszenie" + +#~ msgid "Blocked" +#~ msgstr "Zablokowano" + +#~ msgid "Blocking this instance" +#~ msgstr "Blokujesz tę instancję" + +#~ msgid "User not found" +#~ msgstr "Nie znaleziono użytkownika" + #~ msgid "Conversation muted" #~ msgstr "Wyciszono konwersację" diff --git a/po/regenerate-po-files.sh b/po/regenerate-po-files.sh new file mode 100755 index 0000000..52c1672 --- /dev/null +++ b/po/regenerate-po-files.sh @@ -0,0 +1,3 @@ +cd . +cd build +ninja com.github.bleakgrey.tootle-update-po diff --git a/po/ru.po b/po/ru.po index e1b1966..ce040ee 100644 --- a/po/ru.po +++ b/po/ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.bleakgrey.tootle\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-10-30 19:17+0300\n" +"POT-Creation-Date: 2019-09-16 16:00+0300\n" "PO-Revision-Date: 2018-05-10 00:35+0300\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -19,7 +19,7 @@ msgstr "" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: data/com.github.bleakgrey.tootle.desktop.in:4 -#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 src/MainWindow.vala:68 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 msgid "Tootle" msgstr "" @@ -41,30 +41,34 @@ msgid "Lightning fast client for Mastodon" msgstr "Молниеносный клиент для Мастодонта" #: data/com.github.bleakgrey.tootle.appdata.xml.in:11 +#, fuzzy msgid "" "Tootle is a client for the world’s largest free, open-source, decentralized " -"microblogging network with real-time notifications and multiple accounts " -"support." +"microblogging network with real-time notifications and support for multiple " +"accounts." msgstr "" "Tootle - клиент для крупнейшей в мире свободной, децентрализованной сети " "микроблогинга с открытым исходным кодом. Он поддерживает уведомления в " "реальном времени и несколько рабочих аккаунтов." #: data/com.github.bleakgrey.tootle.appdata.xml.in:14 +#, fuzzy msgid "" -"Mastodon is lovely crafted with power and speed in mind, resulting in a " -"free, independent and popular alternative to the centralized social networks." +"Mastodon is lovingly crafted with power and speed in mind, resulting in a " +"free, independent, and popular alternative to the centralized social " +"networks." msgstr "" "Мастодонт был создан с учётом множества возможностей и скорости, что привело " "к созданию свободной альтернативы популярным централизованным социальным " "сетям." #: data/com.github.bleakgrey.tootle.appdata.xml.in:17 +#, fuzzy msgid "" -"Anyone can run a server of Mastodon. Each server hosts individual user " -"accounts, the content they produce, and the content they are subscribed. " -"Every user can follow each other and share their posts regardless of their " -"server." +"Anyone can run a Mastodon server. Each server hosts individual user " +"accounts, the content they produce, and the content to which they are " +"subscribed. Every user can follow each other and share their posts " +"regardless of their server." msgstr "" "Каждый может запустить свою копию Мастодонта. Сервер хранит аккаунты " "пользователей, контент, который они создают, и контент, на который они " @@ -75,344 +79,280 @@ msgstr "" msgid "bleak_grey" msgstr "" -#: src/Desktop.vala:10 src/API/Account.vala:123 src/API/Account.vala:142 -#: src/API/Account.vala:161 src/Dialogs/NewAccountDialog.vala:102 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:80 +#, fuzzy +msgid "Added Watchlist" +msgstr "Список Наблюдения" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:81 +msgid "Added Redraft support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:82 +msgid "Added Pinning support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:83 +msgid "Added Simplified Chinese and German translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:84 +msgid "Added --hidden Start Flag" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:85 +msgid "Added Shortcuts and Back mouse button support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:86 +msgid "Changed Notifications screen behavior" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:87 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:102 +msgid "Fixed minor bugs" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:94 +msgid "Added Russian, French and Polish translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:95 +#, fuzzy +msgid "Added Direct timeline" +msgstr "Обновлять публичные ленты:" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:96 +msgid "Added support for custom character limit" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:97 +msgid "Added support for streaming all timelines" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:98 +msgid "Added tooltips for image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:99 +msgid "Added remove action for attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:100 +msgid "Changed behavior for mentioning users" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:101 +msgid "Changed behavior for missing image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:109 +msgid "Initial release" +msgstr "" + +#: src/Desktop.vala:10 msgid "Error" msgstr "Ошибка" -#: src/Desktop.vala:46 +#: src/Desktop.vala:47 msgid "Media downloaded" msgstr "Медиаконтент загружен" -#: src/MainWindow.vala:48 -msgid "Back" -msgstr "Назад" - -#: src/MainWindow.vala:54 src/Dialogs/PostDialog.vala:29 -msgid "Toot" -msgstr "Статус" - -#: src/Network.vala:58 -msgid "TLS Error" -msgstr "Ошибка TLS" - -#: src/Network.vala:58 -msgid "Can't ensure secure connection: " -msgstr "Не удалось установить безопасное соединение:" - -#: src/Network.vala:66 +#: src/Services/Accounts.vala:31 #, c-format -msgid "Error: %s" -msgstr "Ошибка: %s" +msgid "" +"This instance has invalidated this session. Please sign in again.\n" +"\n" +"%s" +msgstr "" -#: src/API/NotificationType.vala:50 -#, c-format -msgid "%s mentioned you" -msgstr "%s упомянул вас" - -#: src/API/NotificationType.vala:52 -#, c-format -msgid "%s boosted your toot" -msgstr "%s продвинул ваш статус" - -#: src/API/NotificationType.vala:54 -#, c-format -msgid "%s favorited your toot" -msgstr "%s понравился ваш статус" - -#: src/API/NotificationType.vala:56 -#, c-format -msgid "%s now follows you" -msgstr "%s подписался на вас" - -#: src/API/NotificationType.vala:58 -#, c-format -msgid "%s wants to follow you" -msgstr "%s хочет на вас подписаться" - -#: src/API/NotificationType.vala:60 -#, c-format -msgid "%s posted a toot" -msgstr "%s опубликовал статус" - -#: src/API/Status.vala:174 -msgid "Boosted!" -msgstr "Продвинуто!" - -#: src/API/Status.vala:176 -msgid "Removed boost" -msgstr "Продвижение отменено" - -#: src/API/Status.vala:189 -msgid "Favorited!" -msgstr "Добавлено в понравившиеся!" - -#: src/API/Status.vala:191 -msgid "Removed from favorites" -msgstr "Удалено из понравившихся" - -#: src/API/Status.vala:204 -msgid "Muted!" -msgstr "Заглушено!" - -#: src/API/Status.vala:206 -msgid "Conversation unmuted" -msgstr "Переписка включена" - -#: src/API/Status.vala:219 -msgid "Pinned!" -msgstr "Закреплено!" - -#: src/API/Status.vala:221 -msgid "Unpinned from profile" -msgstr "Откреплено от профиля" - -#: src/API/Status.vala:231 -msgid "Poof!" -msgstr "Вжух!" - -#: src/API/StatusVisibility.vala:40 -msgid "Post to public timelines" -msgstr "Видно в публичных лентах" - -#: src/API/StatusVisibility.vala:42 -msgid "Don't post to public timelines" -msgstr "Не видно в публичных лентах" - -#: src/API/StatusVisibility.vala:44 -msgid "Post to followers only" -msgstr "Только для подписчиков" - -#: src/API/StatusVisibility.vala:46 -msgid "Post to mentioned users only" -msgstr "Только для упомянутых" - -#: src/Widgets/AccountsButton.vala:67 -msgid "Refresh" -msgstr "Обновить" - -#: src/Widgets/AccountsButton.vala:71 -msgid "Favorites" -msgstr "Понравившиеся" - -#: src/Widgets/AccountsButton.vala:75 src/Views/DirectView.vala:12 -msgid "Direct Messages" -msgstr "Личные Сообщения" - -#: src/Widgets/AccountsButton.vala:79 src/Views/SearchView.vala:12 -msgid "Search" -msgstr "Поиск" - -#: src/Widgets/AccountsButton.vala:83 -msgid "Watchlist" -msgstr "Список Наблюдения" - -#: src/Widgets/AccountsButton.vala:87 src/Dialogs/SettingsDialog.vala:18 -msgid "Settings" -msgstr "Настройки" - -#: src/Widgets/AccountsButton.vala:142 -msgid "New Account" -msgstr "Новый аккаунт" - -#: src/Widgets/AccountsButton.vala:143 -msgid "Click to add" -msgstr "Нажмите, чтобы добавить" - -#: src/Widgets/AccountWidget.vala:24 src/Widgets/AttachmentWidget.vala:130 -#: src/Widgets/StatusWidget.vala:289 -msgid "Open in Browser" -msgstr "Открыть в Браузере" - -#: src/Widgets/AccountWidget.vala:26 src/Widgets/AttachmentWidget.vala:132 -#: src/Widgets/StatusWidget.vala:291 -msgid "Copy Link" -msgstr "Скопировать Ссылку" - -#: src/Widgets/AttachmentBox.vala:41 -msgid "Select media files to add" -msgstr "Выберите файлы для добавления" - -#: src/Widgets/AttachmentBox.vala:44 -msgid "_Cancel" -msgstr "_Отмена" - -#: src/Widgets/AttachmentBox.vala:46 -msgid "_Open" -msgstr "_Выбрать" - -#: src/Widgets/AttachmentWidget.vala:67 -#, c-format -msgid "Click to open %s media" -msgstr "Нажмите, чтобы открыть %s" - -#: src/Widgets/AttachmentWidget.vala:84 -msgid "Uploading..." -msgstr "Загрузка..." - -#: src/Widgets/AttachmentWidget.vala:105 -msgid "File read error" -msgstr "Ошибка чтения файла" - -#: src/Widgets/AttachmentWidget.vala:105 -#, c-format -msgid "Can't read file %s: %s" -msgstr "Не удалось прочитать файл %s: %s" - -#: src/Widgets/AttachmentWidget.vala:124 -msgid "Remove" -msgstr "Удалить" - -#: src/Widgets/AttachmentWidget.vala:134 -msgid "Download" -msgstr "Скачать" - -#: src/Widgets/NotificationWidget.vala:20 -msgid "Unknown Notification" -msgstr "Неизвестное уведомление" - -#: src/Widgets/NotificationWidget.vala:25 -msgid "Dismiss" -msgstr "Скрыть" - -#: src/Widgets/NotificationWidget.vala:64 -msgid "Accept" -msgstr "Принять" - -#: src/Widgets/NotificationWidget.vala:66 -msgid "Reject" -msgstr "Отклонить" - -#: src/Widgets/StatusWidget.vala:84 -msgid "Boost" -msgstr "Продвинуть" - -#: src/Widgets/StatusWidget.vala:91 -msgid "Favorite" -msgstr "Нравится" - -#: src/Widgets/StatusWidget.vala:98 -msgid "Reply" -msgstr "Ответить" - -#: src/Widgets/StatusWidget.vala:136 -#, c-format -msgid "%s boosted" -msgstr "%s продвинул" - -#: src/Widgets/StatusWidget.vala:151 -msgid "Toggle content" -msgstr "Развернуть" - -#: src/Widgets/StatusWidget.vala:165 -msgid "[ This post contains sensitive content ]" -msgstr "[ Данный статус содержит чувствительный контент ]" - -#: src/Widgets/StatusWidget.vala:234 -msgid "This post can't be boosted" -msgstr "Этот статус нельзя продвинуть" - -#: src/Widgets/StatusWidget.vala:287 -msgid "Unmute Conversation" -msgstr "Включить переписку" - -#: src/Widgets/StatusWidget.vala:287 -msgid "Mute Conversation" -msgstr "Заглушить Переписку" - -#: src/Widgets/StatusWidget.vala:293 -msgid "Copy Text" -msgstr "Скопировать Текст" - -#: src/Widgets/StatusWidget.vala:300 -msgid "Unpin from Profile" -msgstr "Открепить от Профиля" - -#: src/Widgets/StatusWidget.vala:300 -msgid "Pin on Profile" -msgstr "Закрепить на Профиле" - -#: src/Widgets/StatusWidget.vala:304 -msgid "Delete" -msgstr "Удалить" - -#: src/Widgets/StatusWidget.vala:308 src/Dialogs/PostDialog.vala:72 -msgid "Redraft" -msgstr "Исправить" - -#: src/Dialogs/NewAccountDialog.vala:27 -msgid "New Account" -msgstr "Новый аккаунт" - -#: src/Dialogs/NewAccountDialog.vala:38 -msgid "What's an instance?" -msgstr "Что такое узел?" - -#: src/Dialogs/NewAccountDialog.vala:42 -msgid "Code:" -msgstr "Код:" - -#: src/Dialogs/NewAccountDialog.vala:46 -msgid "Paste your instance authorization code here" -msgstr "Вставьте свой код авторизации здесь" - -#: src/Dialogs/NewAccountDialog.vala:49 -msgid "Add Account" -msgstr "Добавить Аккаунт" - -#: src/Dialogs/NewAccountDialog.vala:60 -msgid "Instance:" -msgstr "Узел:" - -#: src/Dialogs/NewAccountDialog.vala:102 -msgid "Please paste valid instance authorization code" -msgstr "Пожалуйста, вставьте корректный код авторизации" - -#: src/Dialogs/NewAccountDialog.vala:110 +#: src/Services/Network.vala:86 msgid "Network Error" msgstr "Сетевая Ошибка" -#: src/Dialogs/PostDialog.vala:45 -msgid "Post Visibility" -msgstr "Видимость Статуса" +#: src/API/Visibility.vala:36 +msgid "Unlisted" +msgstr "" -#: src/Dialogs/PostDialog.vala:52 -msgid "Add Media" -msgstr "Добавить Медиаконтент" +#: src/API/Visibility.vala:38 +#, fuzzy +msgid "Followers-only" +msgstr "Подписчиков" -#: src/Dialogs/PostDialog.vala:61 -msgid "Spoiler Warning" -msgstr "Спойлер" +#: src/API/Visibility.vala:40 +msgid "Direct" +msgstr "" -#: src/Dialogs/PostDialog.vala:68 -msgid "Cancel" -msgstr "Отмена" +#: src/API/Visibility.vala:42 +msgid "Public" +msgstr "" -#: src/Dialogs/PostDialog.vala:77 -msgid "Toot!" -msgstr "Отправить!" +#: src/API/Visibility.vala:49 +msgid "Don't post to public timelines" +msgstr "Не видно в публичных лентах" -#: src/Dialogs/PostDialog.vala:85 -msgid "Write your warning here" -msgstr "Напишите здесь предупреждение" +#: src/API/Visibility.vala:51 +msgid "Post to followers only" +msgstr "Только для подписчиков" -#: src/Dialogs/SettingsDialog.vala:37 +#: src/API/Visibility.vala:53 +msgid "Post to mentioned users only" +msgstr "Только для упомянутых" + +#: src/API/Visibility.vala:55 +msgid "Post to public timelines" +msgstr "Видно в публичных лентах" + +#: src/API/NotificationType.vala:56 +#, fuzzy, c-format +msgid "" +"%s mentioned you" +msgstr "%s упомянул вас" + +#: src/API/NotificationType.vala:58 +#, fuzzy, c-format +msgid "" +"%s boosted your status" +msgstr "%s продвинул ваш статус" + +#: src/API/NotificationType.vala:60 +#, fuzzy, c-format +msgid "%s boosted" +msgstr "%s продвинул" + +#: src/API/NotificationType.vala:62 +#, fuzzy, c-format +msgid "" +"%s favorited your status" +msgstr "%s понравился ваш статус" + +#: src/API/NotificationType.vala:64 +#, fuzzy, c-format +msgid "" +"%s now follows you" +msgstr "%s подписался на вас" + +#: src/API/NotificationType.vala:66 +#, fuzzy, c-format +msgid "" +"%s wants to follow you" +msgstr "%s хочет на вас подписаться" + +#: src/API/NotificationType.vala:68 +#, fuzzy, c-format +msgid "" +"%s posted a status" +msgstr "%s опубликовал статус" + +#: src/Widgets/RichLabel.vala:105 src/Widgets/Status.vala:206 +#: src/Widgets/Account.vala:28 +msgid "Open in Browser" +msgstr "Открыть в Браузере" + +#: src/Widgets/AccountsButton.vala:62 +msgid "Refresh" +msgstr "Обновить" + +#: src/Widgets/AccountsButton.vala:67 +msgid "Favorites" +msgstr "Понравившиеся" + +#: src/Widgets/AccountsButton.vala:71 src/Views/Direct.vala:12 +msgid "Direct Messages" +msgstr "Личные Сообщения" + +#: src/Widgets/AccountsButton.vala:75 src/Views/Search.vala:12 +msgid "Search" +msgstr "Поиск" + +#: src/Widgets/AccountsButton.vala:79 src/Dialogs/WatchlistEditor.vala:99 +msgid "Watchlist" +msgstr "Список Наблюдения" + +#: src/Widgets/AccountsButton.vala:83 src/Dialogs/Preferences.vala:17 +msgid "Settings" +msgstr "Настройки" + +#: src/Widgets/Status.vala:52 +msgid "[ Show more ]" +msgstr "" + +#: src/Widgets/Status.vala:120 +msgid "This post can't be boosted" +msgstr "Этот статус нельзя продвинуть" + +#: src/Widgets/Status.vala:208 src/Widgets/Account.vala:30 +msgid "Copy Link" +msgstr "Скопировать Ссылку" + +#: src/Widgets/Status.vala:210 +msgid "Copy Text" +msgstr "Скопировать Текст" + +#: src/Widgets/Status.vala:217 +msgid "Unpin from Profile" +msgstr "Открепить от Профиля" + +#: src/Widgets/Status.vala:217 +msgid "Pin on Profile" +msgstr "Закрепить на Профиле" + +#: src/Widgets/Status.vala:221 +msgid "Delete" +msgstr "Удалить" + +#: src/Widgets/Status.vala:225 src/Dialogs/Compose.vala:73 +msgid "Redraft" +msgstr "Исправить" + +#: src/Widgets/Attachment/Box.vala:28 +msgid "Select media files to add" +msgstr "Выберите файлы для добавления" + +#: src/Widgets/Attachment/Box.vala:31 +msgid "_Cancel" +msgstr "_Отмена" + +#: src/Widgets/Attachment/Box.vala:33 +msgid "_Open" +msgstr "_Выбрать" + +#: src/Dialogs/MainWindow.vala:49 +msgid "Back" +msgstr "Назад" + +#: src/Dialogs/MainWindow.vala:58 +msgid "Toot" +msgstr "Статус" + +#: src/Dialogs/MainWindow.vala:63 +msgid "Toggle content" +msgstr "Развернуть" + +#: src/Dialogs/Compose.vala:69 +msgid "Post" +msgstr "" + +#: src/Dialogs/Preferences.vala:36 msgid "Appearance" msgstr "Внешний вид" -#: src/Dialogs/SettingsDialog.vala:38 +#: src/Dialogs/Preferences.vala:37 msgid "Dark theme:" msgstr "Тёмная тема:" -#: src/Dialogs/SettingsDialog.vala:41 +#: src/Dialogs/Preferences.vala:40 msgid "Timelines" msgstr "Ленты" -#: src/Dialogs/SettingsDialog.vala:42 +#: src/Dialogs/Preferences.vala:41 msgid "Real-time updates:" msgstr "Обновления в реальном времени:" -#: src/Dialogs/SettingsDialog.vala:44 +#: src/Dialogs/Preferences.vala:43 msgid "Update public timelines:" msgstr "Обновлять публичные ленты:" @@ -420,33 +360,33 @@ msgstr "Обновлять публичные ленты:" #. grid.attach (new SettingsLabel (_("Use cache:")), 0, i); #. grid.attach (new SettingsSwitch ("cache"), 1, i++); #. grid.attach (new SettingsLabel (_("Max cache size (MB):")), 0, i); -#. var cache_size = new Gtk.SpinButton.with_range (16, 256, 1); +#. var cache_size = new SpinButton.with_range (16, 256, 1); #. settings.schema.bind ("cache-size", cache_size, "value", SettingsBindFlags.DEFAULT); #. grid.attach (cache_size, 1, i++); -#: src/Dialogs/SettingsDialog.vala:55 src/Views/NotificationsView.vala:34 +#: src/Dialogs/Preferences.vala:54 src/Views/Notifications.vala:33 msgid "Notifications" msgstr "Уведомления" -#: src/Dialogs/SettingsDialog.vala:56 +#: src/Dialogs/Preferences.vala:55 msgid "Display notifications:" msgstr "Отображать уведомления:" -#: src/Dialogs/SettingsDialog.vala:58 +#: src/Dialogs/Preferences.vala:57 msgid "Always receive notifications:" msgstr "Всегда получать уведомления:" -#: src/Dialogs/SettingsDialog.vala:64 +#: src/Dialogs/Preferences.vala:63 src/Dialogs/WatchlistEditor.vala:162 msgid "_Close" msgstr "_Закрыть" -#: src/Dialogs/WatchlistDialog.vala:20 +#: src/Dialogs/WatchlistEditor.vala:20 msgid "" "You'll be notified when toots from this user appear in your Home timeline." msgstr "" "Вы получите уведомление, когда статусы от этого пользователя появятся в " "вашей Главной ленте." -#: src/Dialogs/WatchlistDialog.vala:21 +#: src/Dialogs/WatchlistEditor.vala:21 msgid "" "You'll be notified when toots with this hashtag appear in any public " "timelines." @@ -454,110 +394,88 @@ msgstr "" "Вы получите уведомление, когда статусы с данным хэштегом появятся в любой " "публичной ленте." -#: src/Dialogs/WatchlistDialog.vala:137 +#: src/Dialogs/WatchlistEditor.vala:108 msgid "Users" msgstr "Пользователи" -#: src/Dialogs/WatchlistDialog.vala:138 src/Views/SearchView.vala:100 +#: src/Dialogs/WatchlistEditor.vala:109 src/Views/Search.vala:100 msgid "Hashtags" msgstr "Хэштеги" -#: src/Dialogs/WatchlistDialog.vala:148 +#: src/Dialogs/WatchlistEditor.vala:122 msgid "Add" msgstr "Добавить" -#: src/Views/AbstractView.vala:59 +#: src/Views/Base.vala:6 msgid "Nothing to see here" msgstr "Тут ничего нет" -#: src/Views/AccountView.vala:79 -msgid "Edit Profile" -msgstr "Редактировать Профиль" +#: src/Views/NewAccount.vala:91 +msgid "Instance URL is invalid" +msgstr "" -#: src/Views/AccountView.vala:80 -msgid "Mention" -msgstr "Упомянуть" +#: src/Views/NewAccount.vala:133 +#, fuzzy +msgid "Please paste a valid authorization code" +msgstr "Пожалуйста, вставьте корректный код авторизации" -#: src/Views/AccountView.vala:81 -msgid "Report" -msgstr "Пожаловаться" - -#: src/Views/AccountView.vala:82 src/Views/AccountView.vala:167 -msgid "Mute" -msgstr "Заглушить" - -#: src/Views/AccountView.vala:83 src/Views/AccountView.vala:166 -msgid "Block" -msgstr "Заблокировать" - -#: src/Views/AccountView.vala:95 -msgid "More Actions" -msgstr "Больше Действий" - -#: src/Views/AccountView.vala:115 -msgid "Toots" -msgstr "Статусов" - -#: src/Views/AccountView.vala:116 -msgid "Follows" -msgstr "Подписок" - -#: src/Views/AccountView.vala:120 -msgid "Followers" -msgstr "Подписчиков" - -#: src/Views/AccountView.vala:155 -msgid "Unfollow" -msgstr "Отписаться" - -#: src/Views/AccountView.vala:159 -msgid "Follow" -msgstr "Подписаться" - -#: src/Views/AccountView.vala:166 -msgid "Unblock" -msgstr "Разблокировать" - -#: src/Views/AccountView.vala:167 -msgid "Unmute" -msgstr "Включить" - -#: src/Views/AccountView.vala:228 -msgid "Sent follow request" -msgstr "Отправлен запрос на подписку" - -#: src/Views/AccountView.vala:230 -msgid "Blocked" -msgstr "Заблокирован" - -#: src/Views/AccountView.vala:232 -msgid "Follows you" -msgstr "Подписан на вас" - -#: src/Views/AccountView.vala:234 -msgid "Blocking this instance" -msgstr "Данный узел заблокирован" - -#: src/Views/AccountView.vala:269 -msgid "User not found" -msgstr "Пользователь не найден" - -#: src/Views/FederatedView.vala:12 -msgid "Federated Timeline" -msgstr "Глобальная Лента" - -#: src/Views/HomeView.vala:12 src/Views/TimelineView.vala:36 +#: src/Views/Timeline.vala:34 src/Views/Home.vala:12 msgid "Home" msgstr "Главная" -#: src/Views/LocalView.vala:12 +#: src/Views/Local.vala:12 msgid "Local Timeline" msgstr "Локальная Лента" -#: src/Views/SearchView.vala:82 +#: src/Views/Federated.vala:12 +msgid "Federated Timeline" +msgstr "Глобальная Лента" + +#: src/Views/Profile.vala:61 +#, c-format +msgid "%s Posts" +msgstr "" + +#: src/Views/Profile.vala:67 +#, fuzzy, c-format +msgid "%s Follows" +msgstr "Подписок" + +#: src/Views/Profile.vala:73 +#, fuzzy, c-format +msgid "%s Followers" +msgstr "Подписчиков" + +#: src/Views/Profile.vala:109 +msgid "Sent follow request" +msgstr "Отправлен запрос на подписку" + +#: src/Views/Profile.vala:111 +#, fuzzy +msgid "Mutually follows you" +msgstr "Подписан на вас" + +#: src/Views/Profile.vala:113 +msgid "Follows you" +msgstr "Подписан на вас" + +#: src/Views/Profile.vala:124 +#, fuzzy +msgid "Follow back" +msgstr "Подписаться" + +#: src/Views/Profile.vala:126 +msgid "Unfollow" +msgstr "Отписаться" + +#: src/Views/Profile.vala:128 +msgid "Follow" +msgstr "Подписаться" + +#: src/Views/Search.vala:82 msgid "Accounts" msgstr "Аккаунты" -#: src/Views/SearchView.vala:91 +#: src/Views/Search.vala:91 msgid "Statuses" msgstr "Статусы" diff --git a/po/zh_CN.po b/po/zh_CN.po index 83bb1cb..de210fc 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.bleakgrey.tootle\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-10-30 19:17+0300\n" +"POT-Creation-Date: 2019-09-16 16:00+0300\n" "PO-Revision-Date: 2018-10-31 14:47+1300\n" "Last-Translator: \n" "Language-Team: \n" @@ -18,7 +18,7 @@ msgstr "" "Plural-Forms: nplurals=1; plural=0;\n" #: data/com.github.bleakgrey.tootle.desktop.in:4 -#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 src/MainWindow.vala:68 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:7 msgid "Tootle" msgstr "Tootle" @@ -40,28 +40,32 @@ msgid "Lightning fast client for Mastodon" msgstr "轻量快速的 Mastodon 客户端" #: data/com.github.bleakgrey.tootle.appdata.xml.in:11 +#, fuzzy msgid "" "Tootle is a client for the world’s largest free, open-source, decentralized " -"microblogging network with real-time notifications and multiple accounts " -"support." +"microblogging network with real-time notifications and support for multiple " +"accounts." msgstr "" "Tootle 是一个社交应用,基于世界上最大的自由、开源、去中心化社交网络,支持实时" "通知和多帐户登录。" #: data/com.github.bleakgrey.tootle.appdata.xml.in:14 +#, fuzzy msgid "" -"Mastodon is lovely crafted with power and speed in mind, resulting in a " -"free, independent and popular alternative to the centralized social networks." +"Mastodon is lovingly crafted with power and speed in mind, resulting in a " +"free, independent, and popular alternative to the centralized social " +"networks." msgstr "" "Mastodon 以人为本,搭配与生俱来的极致性能,给你一个自由而流行的去中心化社交网" "络。" #: data/com.github.bleakgrey.tootle.appdata.xml.in:17 +#, fuzzy msgid "" -"Anyone can run a server of Mastodon. Each server hosts individual user " -"accounts, the content they produce, and the content they are subscribed. " -"Every user can follow each other and share their posts regardless of their " -"server." +"Anyone can run a Mastodon server. Each server hosts individual user " +"accounts, the content they produce, and the content to which they are " +"subscribed. Every user can follow each other and share their posts " +"regardless of their server." msgstr "" "任何人都可以运营 Mastodon 的节点,各自负责节点上的用户数据。用户可以互相关" "注、分享,而这一切都可以跨节点运作。" @@ -70,344 +74,280 @@ msgstr "" msgid "bleak_grey" msgstr "bleak_grey" -#: src/Desktop.vala:10 src/API/Account.vala:123 src/API/Account.vala:142 -#: src/API/Account.vala:161 src/Dialogs/NewAccountDialog.vala:102 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:80 +#, fuzzy +msgid "Added Watchlist" +msgstr "特别关注" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:81 +msgid "Added Redraft support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:82 +msgid "Added Pinning support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:83 +msgid "Added Simplified Chinese and German translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:84 +msgid "Added --hidden Start Flag" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:85 +msgid "Added Shortcuts and Back mouse button support" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:86 +msgid "Changed Notifications screen behavior" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:87 +#: data/com.github.bleakgrey.tootle.appdata.xml.in:102 +msgid "Fixed minor bugs" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:94 +msgid "Added Russian, French and Polish translations" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:95 +#, fuzzy +msgid "Added Direct timeline" +msgstr "更新公共时间线:" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:96 +msgid "Added support for custom character limit" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:97 +msgid "Added support for streaming all timelines" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:98 +msgid "Added tooltips for image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:99 +msgid "Added remove action for attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:100 +msgid "Changed behavior for mentioning users" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:101 +msgid "Changed behavior for missing image attachments" +msgstr "" + +#: data/com.github.bleakgrey.tootle.appdata.xml.in:109 +msgid "Initial release" +msgstr "" + +#: src/Desktop.vala:10 msgid "Error" msgstr "错误" -#: src/Desktop.vala:46 +#: src/Desktop.vala:47 msgid "Media downloaded" msgstr "媒体已下载" -#: src/MainWindow.vala:48 -msgid "Back" -msgstr "返回" - -#: src/MainWindow.vala:54 src/Dialogs/PostDialog.vala:29 -msgid "Toot" -msgstr "发嘟" - -#: src/Network.vala:58 -msgid "TLS Error" -msgstr "TLS 错误" - -#: src/Network.vala:58 -msgid "Can't ensure secure connection: " -msgstr "无法建立安全连接: " - -#: src/Network.vala:66 +#: src/Services/Accounts.vala:31 #, c-format -msgid "Error: %s" -msgstr "错误:%s" +msgid "" +"This instance has invalidated this session. Please sign in again.\n" +"\n" +"%s" +msgstr "" -#: src/API/NotificationType.vala:50 -#, c-format -msgid "%s mentioned you" -msgstr "%s 提及了你" - -#: src/API/NotificationType.vala:52 -#, c-format -msgid "%s boosted your toot" -msgstr "%s 转了你的嘟文" - -#: src/API/NotificationType.vala:54 -#, c-format -msgid "%s favorited your toot" -msgstr "%s 赞了你的嘟文" - -#: src/API/NotificationType.vala:56 -#, c-format -msgid "%s now follows you" -msgstr "%s 关注了你" - -#: src/API/NotificationType.vala:58 -#, c-format -msgid "%s wants to follow you" -msgstr "%s 想要关注你" - -#: src/API/NotificationType.vala:60 -#, c-format -msgid "%s posted a toot" -msgstr "%s 发了一条嘟文" - -#: src/API/Status.vala:174 -msgid "Boosted!" -msgstr "转!" - -#: src/API/Status.vala:176 -msgid "Removed boost" -msgstr "转嘟已取消" - -#: src/API/Status.vala:189 -msgid "Favorited!" -msgstr "赞!" - -#: src/API/Status.vala:191 -msgid "Removed from favorites" -msgstr "赞已撤回" - -#: src/API/Status.vala:204 -msgid "Muted!" -msgstr "已隐藏!" - -#: src/API/Status.vala:206 -msgid "Conversation unmuted" -msgstr "对话已恢复" - -#: src/API/Status.vala:219 -msgid "Pinned!" -msgstr "已置顶!" - -#: src/API/Status.vala:221 -msgid "Unpinned from profile" -msgstr "取消置顶" - -#: src/API/Status.vala:231 -msgid "Poof!" -msgstr "已删除!" - -#: src/API/StatusVisibility.vala:40 -msgid "Post to public timelines" -msgstr "发送到公共时间线" - -#: src/API/StatusVisibility.vala:42 -msgid "Don't post to public timelines" -msgstr "发送到本地时间线" - -#: src/API/StatusVisibility.vala:44 -msgid "Post to followers only" -msgstr "仅限关注者" - -#: src/API/StatusVisibility.vala:46 -msgid "Post to mentioned users only" -msgstr "仅限提及的用户" - -#: src/Widgets/AccountsButton.vala:67 -msgid "Refresh" -msgstr "刷新" - -#: src/Widgets/AccountsButton.vala:71 -msgid "Favorites" -msgstr "点赞" - -#: src/Widgets/AccountsButton.vala:75 src/Views/DirectView.vala:12 -msgid "Direct Messages" -msgstr "私信" - -#: src/Widgets/AccountsButton.vala:79 src/Views/SearchView.vala:12 -msgid "Search" -msgstr "搜索" - -#: src/Widgets/AccountsButton.vala:83 -msgid "Watchlist" -msgstr "特别关注" - -#: src/Widgets/AccountsButton.vala:87 src/Dialogs/SettingsDialog.vala:18 -msgid "Settings" -msgstr "设置" - -#: src/Widgets/AccountsButton.vala:142 -msgid "New Account" -msgstr "新帐户" - -#: src/Widgets/AccountsButton.vala:143 -msgid "Click to add" -msgstr "点击添加" - -#: src/Widgets/AccountWidget.vala:24 src/Widgets/AttachmentWidget.vala:130 -#: src/Widgets/StatusWidget.vala:289 -msgid "Open in Browser" -msgstr "浏览器中打开" - -#: src/Widgets/AccountWidget.vala:26 src/Widgets/AttachmentWidget.vala:132 -#: src/Widgets/StatusWidget.vala:291 -msgid "Copy Link" -msgstr "复制链接" - -#: src/Widgets/AttachmentBox.vala:41 -msgid "Select media files to add" -msgstr "选择媒体文件" - -#: src/Widgets/AttachmentBox.vala:44 -msgid "_Cancel" -msgstr "_取消" - -#: src/Widgets/AttachmentBox.vala:46 -msgid "_Open" -msgstr "_打开" - -#: src/Widgets/AttachmentWidget.vala:67 -#, c-format -msgid "Click to open %s media" -msgstr "点击打开 %s 媒体" - -#: src/Widgets/AttachmentWidget.vala:84 -msgid "Uploading..." -msgstr "上传中…" - -#: src/Widgets/AttachmentWidget.vala:105 -msgid "File read error" -msgstr "文件读取错误" - -#: src/Widgets/AttachmentWidget.vala:105 -#, c-format -msgid "Can't read file %s: %s" -msgstr "无法读取文件 %s: %s" - -#: src/Widgets/AttachmentWidget.vala:124 -msgid "Remove" -msgstr "移除" - -#: src/Widgets/AttachmentWidget.vala:134 -msgid "Download" -msgstr "下载" - -#: src/Widgets/NotificationWidget.vala:20 -msgid "Unknown Notification" -msgstr "未知通知" - -#: src/Widgets/NotificationWidget.vala:25 -msgid "Dismiss" -msgstr "忽略" - -#: src/Widgets/NotificationWidget.vala:64 -msgid "Accept" -msgstr "接受" - -#: src/Widgets/NotificationWidget.vala:66 -msgid "Reject" -msgstr "拒绝" - -#: src/Widgets/StatusWidget.vala:84 -msgid "Boost" -msgstr "转嘟" - -#: src/Widgets/StatusWidget.vala:91 -msgid "Favorite" -msgstr "赞" - -#: src/Widgets/StatusWidget.vala:98 -msgid "Reply" -msgstr "回复" - -#: src/Widgets/StatusWidget.vala:136 -#, c-format -msgid "%s boosted" -msgstr "%s 转嘟了" - -#: src/Widgets/StatusWidget.vala:151 -msgid "Toggle content" -msgstr "切换隐藏状态" - -#: src/Widgets/StatusWidget.vala:165 -msgid "[ This post contains sensitive content ]" -msgstr "[ 此嘟文有敏感内容 ]" - -#: src/Widgets/StatusWidget.vala:234 -msgid "This post can't be boosted" -msgstr "无法转发这条嘟文" - -#: src/Widgets/StatusWidget.vala:287 -msgid "Unmute Conversation" -msgstr "恢复对话" - -#: src/Widgets/StatusWidget.vala:287 -msgid "Mute Conversation" -msgstr "隐藏对话" - -#: src/Widgets/StatusWidget.vala:293 -msgid "Copy Text" -msgstr "复制文本" - -#: src/Widgets/StatusWidget.vala:300 -msgid "Unpin from Profile" -msgstr "取消置顶" - -#: src/Widgets/StatusWidget.vala:300 -msgid "Pin on Profile" -msgstr "置顶" - -#: src/Widgets/StatusWidget.vala:304 -msgid "Delete" -msgstr "删除" - -#: src/Widgets/StatusWidget.vala:308 src/Dialogs/PostDialog.vala:72 -msgid "Redraft" -msgstr "编辑" - -#: src/Dialogs/NewAccountDialog.vala:27 -msgid "New Account" -msgstr "新帐户" - -#: src/Dialogs/NewAccountDialog.vala:38 -msgid "What's an instance?" -msgstr "需要帮助?" - -#: src/Dialogs/NewAccountDialog.vala:42 -msgid "Code:" -msgstr "授权码:" - -#: src/Dialogs/NewAccountDialog.vala:46 -msgid "Paste your instance authorization code here" -msgstr "在此粘贴授权码" - -#: src/Dialogs/NewAccountDialog.vala:49 -msgid "Add Account" -msgstr "添加帐户" - -#: src/Dialogs/NewAccountDialog.vala:60 -msgid "Instance:" -msgstr "实例:" - -#: src/Dialogs/NewAccountDialog.vala:102 -msgid "Please paste valid instance authorization code" -msgstr "请确认授权码是否有效" - -#: src/Dialogs/NewAccountDialog.vala:110 +#: src/Services/Network.vala:86 msgid "Network Error" msgstr "网络错误" -#: src/Dialogs/PostDialog.vala:45 -msgid "Post Visibility" -msgstr "嘟文可见范围" +#: src/API/Visibility.vala:36 +msgid "Unlisted" +msgstr "" -#: src/Dialogs/PostDialog.vala:52 -msgid "Add Media" -msgstr "添加媒体" +#: src/API/Visibility.vala:38 +#, fuzzy +msgid "Followers-only" +msgstr "关注者" -#: src/Dialogs/PostDialog.vala:61 -msgid "Spoiler Warning" -msgstr "内容预警" +#: src/API/Visibility.vala:40 +msgid "Direct" +msgstr "" -#: src/Dialogs/PostDialog.vala:68 -msgid "Cancel" -msgstr "取消" +#: src/API/Visibility.vala:42 +msgid "Public" +msgstr "" -#: src/Dialogs/PostDialog.vala:77 -msgid "Toot!" -msgstr "发嘟!" +#: src/API/Visibility.vala:49 +msgid "Don't post to public timelines" +msgstr "发送到本地时间线" -#: src/Dialogs/PostDialog.vala:85 -msgid "Write your warning here" -msgstr "输入预警提示" +#: src/API/Visibility.vala:51 +msgid "Post to followers only" +msgstr "仅限关注者" -#: src/Dialogs/SettingsDialog.vala:37 +#: src/API/Visibility.vala:53 +msgid "Post to mentioned users only" +msgstr "仅限提及的用户" + +#: src/API/Visibility.vala:55 +msgid "Post to public timelines" +msgstr "发送到公共时间线" + +#: src/API/NotificationType.vala:56 +#, fuzzy, c-format +msgid "" +"%s mentioned you" +msgstr "%s 提及了你" + +#: src/API/NotificationType.vala:58 +#, fuzzy, c-format +msgid "" +"%s boosted your status" +msgstr "%s 转了你的嘟文" + +#: src/API/NotificationType.vala:60 +#, fuzzy, c-format +msgid "%s boosted" +msgstr "%s 转嘟了" + +#: src/API/NotificationType.vala:62 +#, fuzzy, c-format +msgid "" +"%s favorited your status" +msgstr "%s 赞了你的嘟文" + +#: src/API/NotificationType.vala:64 +#, fuzzy, c-format +msgid "" +"%s now follows you" +msgstr "%s 关注了你" + +#: src/API/NotificationType.vala:66 +#, fuzzy, c-format +msgid "" +"%s wants to follow you" +msgstr "%s 想要关注你" + +#: src/API/NotificationType.vala:68 +#, fuzzy, c-format +msgid "" +"%s posted a status" +msgstr "%s 发了一条嘟文" + +#: src/Widgets/RichLabel.vala:105 src/Widgets/Status.vala:206 +#: src/Widgets/Account.vala:28 +msgid "Open in Browser" +msgstr "浏览器中打开" + +#: src/Widgets/AccountsButton.vala:62 +msgid "Refresh" +msgstr "刷新" + +#: src/Widgets/AccountsButton.vala:67 +msgid "Favorites" +msgstr "点赞" + +#: src/Widgets/AccountsButton.vala:71 src/Views/Direct.vala:12 +msgid "Direct Messages" +msgstr "私信" + +#: src/Widgets/AccountsButton.vala:75 src/Views/Search.vala:12 +msgid "Search" +msgstr "搜索" + +#: src/Widgets/AccountsButton.vala:79 src/Dialogs/WatchlistEditor.vala:99 +msgid "Watchlist" +msgstr "特别关注" + +#: src/Widgets/AccountsButton.vala:83 src/Dialogs/Preferences.vala:17 +msgid "Settings" +msgstr "设置" + +#: src/Widgets/Status.vala:52 +msgid "[ Show more ]" +msgstr "" + +#: src/Widgets/Status.vala:120 +msgid "This post can't be boosted" +msgstr "无法转发这条嘟文" + +#: src/Widgets/Status.vala:208 src/Widgets/Account.vala:30 +msgid "Copy Link" +msgstr "复制链接" + +#: src/Widgets/Status.vala:210 +msgid "Copy Text" +msgstr "复制文本" + +#: src/Widgets/Status.vala:217 +msgid "Unpin from Profile" +msgstr "取消置顶" + +#: src/Widgets/Status.vala:217 +msgid "Pin on Profile" +msgstr "置顶" + +#: src/Widgets/Status.vala:221 +msgid "Delete" +msgstr "删除" + +#: src/Widgets/Status.vala:225 src/Dialogs/Compose.vala:73 +msgid "Redraft" +msgstr "编辑" + +#: src/Widgets/Attachment/Box.vala:28 +msgid "Select media files to add" +msgstr "选择媒体文件" + +#: src/Widgets/Attachment/Box.vala:31 +msgid "_Cancel" +msgstr "_取消" + +#: src/Widgets/Attachment/Box.vala:33 +msgid "_Open" +msgstr "_打开" + +#: src/Dialogs/MainWindow.vala:49 +msgid "Back" +msgstr "返回" + +#: src/Dialogs/MainWindow.vala:58 +msgid "Toot" +msgstr "发嘟" + +#: src/Dialogs/MainWindow.vala:63 +msgid "Toggle content" +msgstr "切换隐藏状态" + +#: src/Dialogs/Compose.vala:69 +msgid "Post" +msgstr "" + +#: src/Dialogs/Preferences.vala:36 msgid "Appearance" msgstr "外观" -#: src/Dialogs/SettingsDialog.vala:38 +#: src/Dialogs/Preferences.vala:37 msgid "Dark theme:" msgstr "暗色主题:" -#: src/Dialogs/SettingsDialog.vala:41 +#: src/Dialogs/Preferences.vala:40 msgid "Timelines" msgstr "时间线" -#: src/Dialogs/SettingsDialog.vala:42 +#: src/Dialogs/Preferences.vala:41 msgid "Real-time updates:" msgstr "实时更新:" -#: src/Dialogs/SettingsDialog.vala:44 +#: src/Dialogs/Preferences.vala:43 msgid "Update public timelines:" msgstr "更新公共时间线:" @@ -415,144 +355,284 @@ msgstr "更新公共时间线:" #. grid.attach (new SettingsLabel (_("Use cache:")), 0, i); #. grid.attach (new SettingsSwitch ("cache"), 1, i++); #. grid.attach (new SettingsLabel (_("Max cache size (MB):")), 0, i); -#. var cache_size = new Gtk.SpinButton.with_range (16, 256, 1); +#. var cache_size = new SpinButton.with_range (16, 256, 1); #. settings.schema.bind ("cache-size", cache_size, "value", SettingsBindFlags.DEFAULT); #. grid.attach (cache_size, 1, i++); -#: src/Dialogs/SettingsDialog.vala:55 src/Views/NotificationsView.vala:34 +#: src/Dialogs/Preferences.vala:54 src/Views/Notifications.vala:33 msgid "Notifications" msgstr "通知" -#: src/Dialogs/SettingsDialog.vala:56 +#: src/Dialogs/Preferences.vala:55 msgid "Display notifications:" msgstr "显示通知:" -#: src/Dialogs/SettingsDialog.vala:58 +#: src/Dialogs/Preferences.vala:57 msgid "Always receive notifications:" msgstr "总是接收通知:" -#: src/Dialogs/SettingsDialog.vala:64 +#: src/Dialogs/Preferences.vala:63 src/Dialogs/WatchlistEditor.vala:162 msgid "_Close" msgstr "_关闭" -#: src/Dialogs/WatchlistDialog.vala:20 +#: src/Dialogs/WatchlistEditor.vala:20 msgid "" "You'll be notified when toots from this user appear in your Home timeline." msgstr "接收特别关心对象的通知。" -#: src/Dialogs/WatchlistDialog.vala:21 +#: src/Dialogs/WatchlistEditor.vala:21 msgid "" "You'll be notified when toots with this hashtag appear in any public " "timelines." msgstr "接收特别关注话题的通知。" -#: src/Dialogs/WatchlistDialog.vala:137 +#: src/Dialogs/WatchlistEditor.vala:108 msgid "Users" msgstr "用户" -#: src/Dialogs/WatchlistDialog.vala:138 src/Views/SearchView.vala:100 +#: src/Dialogs/WatchlistEditor.vala:109 src/Views/Search.vala:100 msgid "Hashtags" msgstr "话题" -#: src/Dialogs/WatchlistDialog.vala:148 +#: src/Dialogs/WatchlistEditor.vala:122 msgid "Add" msgstr "添加" -#: src/Views/AbstractView.vala:59 +#: src/Views/Base.vala:6 msgid "Nothing to see here" msgstr "什么也没有" -#: src/Views/AccountView.vala:79 -msgid "Edit Profile" -msgstr "编辑个人资料" +#: src/Views/NewAccount.vala:91 +msgid "Instance URL is invalid" +msgstr "" -#: src/Views/AccountView.vala:80 -msgid "Mention" -msgstr "提及" +#: src/Views/NewAccount.vala:133 +#, fuzzy +msgid "Please paste a valid authorization code" +msgstr "请确认授权码是否有效" -#: src/Views/AccountView.vala:81 -msgid "Report" -msgstr "举报" - -#: src/Views/AccountView.vala:82 src/Views/AccountView.vala:167 -msgid "Mute" -msgstr "隐藏" - -#: src/Views/AccountView.vala:83 src/Views/AccountView.vala:166 -msgid "Block" -msgstr "屏蔽" - -#: src/Views/AccountView.vala:95 -msgid "More Actions" -msgstr "更多" - -#: src/Views/AccountView.vala:115 -msgid "Toots" -msgstr "嘟文" - -#: src/Views/AccountView.vala:116 -msgid "Follows" -msgstr "关注" - -#: src/Views/AccountView.vala:120 -msgid "Followers" -msgstr "关注者" - -#: src/Views/AccountView.vala:155 -msgid "Unfollow" -msgstr "取消关注" - -#: src/Views/AccountView.vala:159 -msgid "Follow" -msgstr "关注" - -#: src/Views/AccountView.vala:166 -msgid "Unblock" -msgstr "取消屏蔽" - -#: src/Views/AccountView.vala:167 -msgid "Unmute" -msgstr "取消隐藏" - -#: src/Views/AccountView.vala:228 -msgid "Sent follow request" -msgstr "已发送关注请求" - -#: src/Views/AccountView.vala:230 -msgid "Blocked" -msgstr "已屏蔽" - -#: src/Views/AccountView.vala:232 -msgid "Follows you" -msgstr "关注了你" - -#: src/Views/AccountView.vala:234 -msgid "Blocking this instance" -msgstr "已屏蔽此实例" - -#: src/Views/AccountView.vala:269 -msgid "User not found" -msgstr "未找到用户" - -#: src/Views/FederatedView.vala:12 -msgid "Federated Timeline" -msgstr "公共时间线" - -#: src/Views/HomeView.vala:12 src/Views/TimelineView.vala:36 +#: src/Views/Timeline.vala:34 src/Views/Home.vala:12 msgid "Home" msgstr "主页" -#: src/Views/LocalView.vala:12 +#: src/Views/Local.vala:12 msgid "Local Timeline" msgstr "本地时间线" -#: src/Views/SearchView.vala:82 +#: src/Views/Federated.vala:12 +msgid "Federated Timeline" +msgstr "公共时间线" + +#: src/Views/Profile.vala:61 +#, c-format +msgid "%s Posts" +msgstr "" + +#: src/Views/Profile.vala:67 +#, fuzzy, c-format +msgid "%s Follows" +msgstr "关注" + +#: src/Views/Profile.vala:73 +#, fuzzy, c-format +msgid "%s Followers" +msgstr "关注者" + +#: src/Views/Profile.vala:109 +msgid "Sent follow request" +msgstr "已发送关注请求" + +#: src/Views/Profile.vala:111 +#, fuzzy +msgid "Mutually follows you" +msgstr "关注了你" + +#: src/Views/Profile.vala:113 +msgid "Follows you" +msgstr "关注了你" + +#: src/Views/Profile.vala:124 +#, fuzzy +msgid "Follow back" +msgstr "关注" + +#: src/Views/Profile.vala:126 +msgid "Unfollow" +msgstr "取消关注" + +#: src/Views/Profile.vala:128 +msgid "Follow" +msgstr "关注" + +#: src/Views/Search.vala:82 msgid "Accounts" msgstr "帐户" -#: src/Views/SearchView.vala:91 +#: src/Views/Search.vala:91 msgid "Statuses" msgstr "嘟文" +#~ msgid "TLS Error" +#~ msgstr "TLS 错误" + +#~ msgid "Can't ensure secure connection: " +#~ msgstr "无法建立安全连接: " + +#~ msgid "Error: %s" +#~ msgstr "错误:%s" + +#~ msgid "Boosted!" +#~ msgstr "转!" + +#~ msgid "Removed boost" +#~ msgstr "转嘟已取消" + +#~ msgid "Favorited!" +#~ msgstr "赞!" + +#~ msgid "Removed from favorites" +#~ msgstr "赞已撤回" + +#~ msgid "Muted!" +#~ msgstr "已隐藏!" + +#~ msgid "Conversation unmuted" +#~ msgstr "对话已恢复" + +#~ msgid "Pinned!" +#~ msgstr "已置顶!" + +#~ msgid "Unpinned from profile" +#~ msgstr "取消置顶" + +#~ msgid "Poof!" +#~ msgstr "已删除!" + +#~ msgid "New Account" +#~ msgstr "新帐户" + +#~ msgid "Click to add" +#~ msgstr "点击添加" + +#~ msgid "Click to open %s media" +#~ msgstr "点击打开 %s 媒体" + +#~ msgid "Uploading..." +#~ msgstr "上传中…" + +#~ msgid "File read error" +#~ msgstr "文件读取错误" + +#~ msgid "Can't read file %s: %s" +#~ msgstr "无法读取文件 %s: %s" + +#~ msgid "Remove" +#~ msgstr "移除" + +#~ msgid "Download" +#~ msgstr "下载" + +#~ msgid "Unknown Notification" +#~ msgstr "未知通知" + +#~ msgid "Dismiss" +#~ msgstr "忽略" + +#~ msgid "Accept" +#~ msgstr "接受" + +#~ msgid "Reject" +#~ msgstr "拒绝" + +#~ msgid "Boost" +#~ msgstr "转嘟" + +#~ msgid "Favorite" +#~ msgstr "赞" + +#~ msgid "Reply" +#~ msgstr "回复" + +#~ msgid "[ This post contains sensitive content ]" +#~ msgstr "[ 此嘟文有敏感内容 ]" + +#~ msgid "Unmute Conversation" +#~ msgstr "恢复对话" + +#~ msgid "Mute Conversation" +#~ msgstr "隐藏对话" + +#~ msgid "New Account" +#~ msgstr "新帐户" + +#~ msgid "What's an instance?" +#~ msgstr "需要帮助?" + +#~ msgid "Code:" +#~ msgstr "授权码:" + +#~ msgid "Paste your instance authorization code here" +#~ msgstr "在此粘贴授权码" + +#~ msgid "Add Account" +#~ msgstr "添加帐户" + +#~ msgid "Instance:" +#~ msgstr "实例:" + +#~ msgid "Post Visibility" +#~ msgstr "嘟文可见范围" + +#~ msgid "Add Media" +#~ msgstr "添加媒体" + +#~ msgid "Spoiler Warning" +#~ msgstr "内容预警" + +#~ msgid "Cancel" +#~ msgstr "取消" + +#~ msgid "Toot!" +#~ msgstr "发嘟!" + +#~ msgid "Write your warning here" +#~ msgstr "输入预警提示" + +#~ msgid "Edit Profile" +#~ msgstr "编辑个人资料" + +#~ msgid "Mention" +#~ msgstr "提及" + +#~ msgid "Report" +#~ msgstr "举报" + +#~ msgid "Mute" +#~ msgstr "隐藏" + +#~ msgid "Block" +#~ msgstr "屏蔽" + +#~ msgid "More Actions" +#~ msgstr "更多" + +#~ msgid "Toots" +#~ msgstr "嘟文" + +#~ msgid "Unblock" +#~ msgstr "取消屏蔽" + +#~ msgid "Unmute" +#~ msgstr "取消隐藏" + +#~ msgid "Blocked" +#~ msgstr "已屏蔽" + +#~ msgid "Blocking this instance" +#~ msgstr "已屏蔽此实例" + +#~ msgid "User not found" +#~ msgstr "未找到用户" + #~ msgid "Conversation muted" #~ msgstr "对话已隐藏" diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000..42fc5a2 --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = false +charset = utf-8 + +[*.sh] +indent_size = 4 +indent_style = tab diff --git a/src/API/Account.vala b/src/API/Account.vala index 0ea45dd..f67ea2a 100644 --- a/src/API/Account.vala +++ b/src/API/Account.vala @@ -1,60 +1,57 @@ -public class Tootle.API.Account { +public class Tootle.API.Account : GLib.Object { - public abstract signal void updated (); - - public int64 id; - public string username; - public string acct; - public string display_name; - public string note; - public string header; - public string avatar; - public string url; - public string created_at; - public int64 followers_count; - public int64 following_count; - public int64 statuses_count; - - public Relationship? rs; - - public Account (int64 _id){ - id = _id; + public int64 id { get; set; } + public string username { get; set; } + public string acct { get; set; } + public string? _display_name = null; + public string display_name { + set { + this._display_name = value; + } + get { + return (_display_name == null || _display_name == "") ? username : _display_name; + } } + public string note { get; set; } + public string header { get; set; } + public string avatar { get; set; } + public string url { get; set; } + public string created_at { get; set; } + public int64 followers_count { get; set; } + public int64 following_count { get; set; } + public int64 posts_count { get; set; } + public Relationship? rs { get; set; default = null; } - public static Account parse(Json.Object obj) { - var id = int64.parse (obj.get_string_member ("id")); - var account = new Account (id); + public Account (Json.Object obj) { + Object ( + id: int64.parse (obj.get_string_member ("id")), + username: obj.get_string_member ("username"), + acct: obj.get_string_member ("acct"), + display_name: obj.get_string_member ("display_name"), + note: obj.get_string_member ("note"), + avatar: obj.get_string_member ("avatar"), + header: obj.get_string_member ("header"), + url: obj.get_string_member ("url"), + created_at: obj.get_string_member ("created_at"), - account.username = obj.get_string_member ("username"); - account.acct = obj.get_string_member ("acct"); - account.display_name = obj.get_string_member ("display_name"); - if (account.display_name == "") - account.display_name = account.username; - account.note = obj.get_string_member ("note"); - account.avatar = obj.get_string_member ("avatar"); - account.header = obj.get_string_member ("header"); - account.url = obj.get_string_member ("url"); - account.created_at = obj.get_string_member ("created_at"); - - account.followers_count = obj.get_int_member ("followers_count"); - account.following_count = obj.get_int_member ("following_count"); - account.statuses_count = obj.get_int_member ("statuses_count"); + followers_count: obj.get_int_member ("followers_count"), + following_count: obj.get_int_member ("following_count"), + posts_count: obj.get_int_member ("statuses_count") + ); if (obj.has_member ("fields")) { obj.get_array_member ("fields").foreach_element ((array, i, node) => { var field_obj = node.get_object (); var field_name = field_obj.get_string_member ("name"); var field_val = field_obj.get_string_member ("value"); - account.note += "\n"; - account.note += field_name + ": "; - account.note += field_val; + note += "\n"; + note += field_name + ": "; + note += field_val; }); } - - return account; } - public Json.Node? serialize () { + public virtual Json.Node? serialize () { var builder = new Json.Builder (); builder.begin_object (); builder.set_member_name ("id"); @@ -66,7 +63,7 @@ public class Tootle.API.Account { builder.set_member_name ("followers_count"); builder.add_int_value (followers_count); builder.set_member_name ("statuses_count"); - builder.add_int_value (statuses_count); + builder.add_int_value (posts_count); builder.set_member_name ("display_name"); builder.add_string_value (display_name); builder.set_member_name ("username"); @@ -86,83 +83,55 @@ public class Tootle.API.Account { return builder.get_root (); } - public bool is_self (){ - return id == accounts.current.id; + public bool is_self () { + return id == accounts.active.id; } - public Soup.Message get_relationship (){ - var url = "%s/api/v1/accounts/relationships?id=%lld".printf (accounts.formal.instance, id); - var msg = new Soup.Message("GET", url); - msg.priority = Soup.MessagePriority.HIGH; - Tootle.network.queue (msg, (sess, mess) => { - try{ - var root = Tootle.network.parse_array (mess).get_object_element (0); - rs = Relationship.parse (root); - updated (); - } - catch (GLib.Error e) { - warning ("Can't get account relationship:"); - warning (e.message); - } - }); - return msg; + public Request get_relationship () { + return new Request.GET ("/api/v1/accounts/relationships") + .with_account (accounts.active) + .with_param ("id", id.to_string ()) + .then_parse_array (node => { + rs = new Relationship (node.get_object ()); + }) + .on_error (network.on_error) + .exec (); } - public Soup.Message set_following (bool follow = true){ - var action = follow ? "follow" : "unfollow"; - var url = "%s/api/v1/accounts/%lld/%s".printf (accounts.formal.instance, id, action); - var msg = new Soup.Message("POST", url); - msg.priority = Soup.MessagePriority.HIGH; - network.queue (msg, (sess, mess) => { - try{ - var root = network.parse (mess); - rs = Relationship.parse (root); - updated (); - } - catch (GLib.Error e) { - app.error (_("Error"), e.message); - warning (e.message); - } - }); - return msg; + public Request set_following (bool state = true) { + var action = state ? "follow" : "unfollow"; + return new Request.POST (@"/api/v1/accounts/$id/$action") + .with_account (accounts.active) + .then ((sess, msg) => { + var root = network.parse (msg); + rs = new Relationship (root); + }) + .on_error (network.on_error) + .exec (); } - public Soup.Message set_muted (bool mute = true){ - var action = mute ? "mute" : "unmute"; - var url = "%s/api/v1/accounts/%lld/%s".printf (accounts.formal.instance, id, action); - var msg = new Soup.Message("POST", url); - msg.priority = Soup.MessagePriority.HIGH; - network.queue (msg, (sess, mess) => { - try{ - var root = network.parse (mess); - rs = Relationship.parse (root); - updated (); - } - catch (GLib.Error e) { - app.error (_("Error"), e.message); - warning (e.message); - } - }); - return msg; + public Request set_muted (bool state = true) { + var action = state ? "mute" : "unmute"; + return new Request.POST (@"/api/v1/accounts/$id/$action") + .with_account (accounts.active) + .then ((sess, msg) => { + var root = network.parse (msg); + rs = new Relationship (root); + }) + .on_error (network.on_error) + .exec (); } - public Soup.Message set_blocked (bool block = true){ - var action = block ? "block" : "unblock"; - var url = "%s/api/v1/accounts/%lld/%s".printf (accounts.formal.instance, id, action); - var msg = new Soup.Message("POST", url); - msg.priority = Soup.MessagePriority.HIGH; - network.queue (msg, (sess, mess) => { - try{ - var root = network.parse (mess); - rs = Relationship.parse (root); - updated (); - } - catch (GLib.Error e) { - app.error (_("Error"), e.message); - warning (e.message); - } - }); - return msg; + public Request set_blocked (bool state = true) { + var action = state ? "block" : "unblock"; + return new Request.POST (@"/api/v1/accounts/$id/$action") + .with_account (accounts.active) + .then ((sess, msg) => { + var root = network.parse (msg); + rs = new Relationship (root); + }) + .on_error (network.on_error) + .exec (); } } diff --git a/src/API/Attachment.vala b/src/API/Attachment.vala index b09f42c..7c40a41 100644 --- a/src/API/Attachment.vala +++ b/src/API/Attachment.vala @@ -1,27 +1,24 @@ -public class Tootle.API.Attachment { +public class Tootle.API.Attachment : GLib.Object { - public int64 id; - public string type; - public string url; - public string preview_url; - public string? description; + public int64 id { get; construct set; } + public string kind { get; set; } + public string url { get; set; } + public string? description { get; set; default = null; } - public Attachment (int64 _id) { - id = _id; + public string? _preview_url = null; + public string preview_url { + set { this._preview_url = value; } + get { return (_preview_url == null || _preview_url == "") ? url : _preview_url; } } - public static Attachment parse (Json.Object obj) { - var id = int64.parse (obj.get_string_member ("id")); - var attachment = new Attachment (id); - - attachment.type = obj.get_string_member ("type"); - attachment.preview_url = obj.get_string_member ("preview_url"); - attachment.url = obj.get_string_member ("url"); - - if (obj.has_member ("description")) - attachment.description = obj.get_string_member ("description"); - - return attachment; + public Attachment (Json.Object obj) { + Object ( + id: int64.parse (obj.get_string_member ("id")), + kind: obj.get_string_member ("type"), + preview_url: obj.get_string_member ("preview_url"), + url: obj.get_string_member ("url"), + description: obj.get_string_member ("description") + ); } public Json.Node? serialize () { @@ -30,7 +27,7 @@ public class Tootle.API.Attachment { builder.set_member_name ("id"); builder.add_string_value (id.to_string ()); builder.set_member_name ("type"); - builder.add_string_value (type); + builder.add_string_value (kind); builder.set_member_name ("url"); builder.add_string_value (url); builder.set_member_name ("preview_url"); diff --git a/src/API/Mention.vala b/src/API/Mention.vala index bb1d219..cee2fce 100644 --- a/src/API/Mention.vala +++ b/src/API/Mention.vala @@ -1,30 +1,26 @@ public class Tootle.API.Mention : GLib.Object { - public int64 id; - public string username; - public string acct; - public string url; + public int64 id { get; construct set; } + public string username { get; construct set; } + public string acct { get; construct set; } + public string url { get; construct set; } - public Mention (int64 _id){ - id = _id; + public Mention (Json.Object obj) { + Object ( + id: int64.parse (obj.get_string_member ("id")), + username: obj.get_string_member ("username"), + acct: obj.get_string_member ("acct"), + url: obj.get_string_member ("url") + ); } - public Mention.from_account (Account account){ - id = account.id; - username = account.username; - acct = account.acct; - url = account.url; - } - - public static Mention parse (Json.Object obj){ - var id = int64.parse (obj.get_string_member ("id")); - var mention = new Mention (id); - - mention.username = obj.get_string_member ("username"); - mention.acct = obj.get_string_member ("acct"); - mention.url = obj.get_string_member ("url"); - - return mention; + public Mention.from_account (Account account) { + Object ( + id: account.id, + username: account.username, + acct: account.acct, + url: account.url + ); } public Json.Node? serialize () { diff --git a/src/API/Notification.vala b/src/API/Notification.vala index 6802166..572a826 100644 --- a/src/API/Notification.vala +++ b/src/API/Notification.vala @@ -1,29 +1,30 @@ -public class Tootle.API.Notification { +public class Tootle.API.Notification : GLib.Object { - public int64 id; - public NotificationType type; - public string created_at; + public int64 id { get; construct set; } + public Account account { get; construct set; } - public Status? status; - public Account? account; + public NotificationType kind { get; set; } + public string created_at { get; set; } + public Status? status { get; set; default = null; } - public Notification (int64 _id) { - id = _id; - } - - public static Notification parse (Json.Object obj) { - var id = int64.parse (obj.get_string_member ("id")); - var notification = new Notification (id); - - notification.type = NotificationType.from_string (obj.get_string_member ("type")); - notification.created_at = obj.get_string_member ("created_at"); + public Notification (Json.Object obj) throws Oopsie { + Object ( + id: int64.parse (obj.get_string_member ("id")), + kind: NotificationType.from_string (obj.get_string_member ("type")), + created_at: obj.get_string_member ("created_at"), + account: new Account (obj.get_object_member ("account")) + ); if (obj.has_member ("status")) - notification.status = Status.parse (obj.get_object_member ("status")); - if (obj.has_member ("account")) - notification.account = Account.parse (obj.get_object_member ("account")); + status = new Status (obj.get_object_member ("status")); + } - return notification; + public Notification.follow_request (Json.Object obj) { + Object ( + id: 0, + kind: NotificationType.FOLLOW_REQUEST, + account: new Account (obj) + ); } public Json.Node? serialize () { @@ -32,7 +33,7 @@ public class Tootle.API.Notification { builder.set_member_name ("id"); builder.add_string_value (id.to_string ()); builder.set_member_name ("type"); - builder.add_string_value (type.to_string ()); + builder.add_string_value (kind.to_string ()); builder.set_member_name ("created_at"); builder.add_string_value (created_at); @@ -49,47 +50,35 @@ public class Tootle.API.Notification { return builder.get_root (); } - public static Notification parse_follow_request (Json.Object obj) { - var notification = new Notification (-1); - var account = Account.parse (obj); - - notification.type = NotificationType.FOLLOW_REQUEST; - notification.account = account; - - return notification; - } - public Soup.Message? dismiss () { - if (type == NotificationType.WATCHLIST) { - if (accounts.formal.cached_notifications.remove (this)) + if (kind == NotificationType.WATCHLIST) { + if (accounts.active.cached_notifications.remove (this)) accounts.save (); return null; } - if (type == NotificationType.FOLLOW_REQUEST) + if (kind == NotificationType.FOLLOW_REQUEST) return reject_follow_request (); - var url = "%s/api/v1/notifications/dismiss?id=%lld".printf (accounts.formal.instance, id); - var msg = new Soup.Message ("POST", url); - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg); - return msg; + var req = new Request.POST ("/api/v1/notifications/dismiss") + .with_account (accounts.active) + .with_param ("id", id.to_string ()) + .exec (); + return req; } public Soup.Message accept_follow_request () { - var url = "%s/api/v1/follow_requests/%lld/authorize".printf (accounts.formal.instance, account.id); - var msg = new Soup.Message ("POST", url); - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg); - return msg; + var req = new Request.POST (@"/api/v1/follow_requests/$(account.id)/authorize") + .with_account (accounts.active) + .exec (); + return req; } public Soup.Message reject_follow_request () { - var url = "%s/api/v1/follow_requests/%lld/reject".printf (accounts.formal.instance, account.id); - var msg = new Soup.Message ("POST", url); - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg); - return msg; + var req = new Request.POST (@"/api/v1/follow_requests/$(account.id)/reject") + .with_account (accounts.active) + .exec (); + return req; } } diff --git a/src/API/NotificationType.vala b/src/API/NotificationType.vala index 0d889fc..18e2a67 100644 --- a/src/API/NotificationType.vala +++ b/src/API/NotificationType.vala @@ -7,7 +7,7 @@ public enum Tootle.API.NotificationType { FOLLOW_REQUEST, // Internal WATCHLIST; // Internal - public string to_string() { + public string to_string () { switch (this) { case MENTION: return "mention"; @@ -24,11 +24,12 @@ public enum Tootle.API.NotificationType { case WATCHLIST: return "watchlist"; default: - assert_not_reached(); + warning (@"Unknown notification type: $this"); + return ""; } } - public static NotificationType from_string (string str) { + public static NotificationType from_string (string str) throws Oopsie { switch (str) { case "mention": return MENTION; @@ -45,7 +46,7 @@ public enum Tootle.API.NotificationType { case "watchlist": return WATCHLIST; default: - assert_not_reached(); + throw new Oopsie.INSTANCE (@"Unknown notification type: $str"); } } @@ -54,19 +55,20 @@ public enum Tootle.API.NotificationType { case MENTION: return _("%s mentioned you").printf (account.url, account.display_name); case REBLOG: - return _("%s boosted your toot").printf (account.url, account.display_name); + return _("%s boosted your status").printf (account.url, account.display_name); case REBLOG_REMOTE_USER: return _("%s boosted").printf (account.url, account.display_name); case FAVORITE: - return _("%s favorited your toot").printf (account.url, account.display_name); + return _("%s favorited your status").printf (account.url, account.display_name); case FOLLOW: return _("%s now follows you").printf (account.url, account.display_name); case FOLLOW_REQUEST: return _("%s wants to follow you").printf (account.url, account.display_name); case WATCHLIST: - return _("%s posted a toot").printf (account.url, account.display_name); + return _("%s posted a status").printf (account.url, account.display_name); default: - assert_not_reached(); + warning (@"Unknown notification type: $this"); + return ""; } } @@ -76,6 +78,7 @@ public enum Tootle.API.NotificationType { case WATCHLIST: return "user-available-symbolic"; case REBLOG: + case REBLOG_REMOTE_USER: return "media-playlist-repeat-symbolic"; case FAVORITE: return "emblem-favorite-symbolic"; @@ -83,7 +86,8 @@ public enum Tootle.API.NotificationType { case FOLLOW_REQUEST: return "contact-new-symbolic"; default: - assert_not_reached(); + warning (@"Unknown notification type: $this"); + return ""; } } diff --git a/src/API/Relationship.vala b/src/API/Relationship.vala index eae29cf..3d6580a 100644 --- a/src/API/Relationship.vala +++ b/src/API/Relationship.vala @@ -1,31 +1,25 @@ -using GLib; +public class Tootle.API.Relationship : GLib.Object { -public class Tootle.API.Relationship : Object { + public int64 id { get; construct set; } + public bool following { get; set; default = false; } + public bool followed_by { get; set; default = false; } + public bool muting { get; set; default = false; } + public bool muting_notifications { get; set; default = false; } + public bool requested { get; set; default = false; } + public bool blocking { get; set; default = false; } + public bool domain_blocking { get; set; default = false; } - public int64 id; - public bool following; - public bool followed_by; - public bool blocking; - public bool muting; - public bool muting_notifications; - public bool requested; - public bool domain_blocking; - - public Relationship (int64 _id) { - id = _id; - } - - public static Relationship parse (Json.Object obj) { - var id = int64.parse (obj.get_string_member ("id")); - var relationship = new Relationship (id); - relationship.following = obj.get_boolean_member ("following"); - relationship.followed_by = obj.get_boolean_member ("followed_by"); - relationship.blocking = obj.get_boolean_member ("blocking"); - relationship.muting = obj.get_boolean_member ("muting"); - relationship.muting_notifications = obj.get_boolean_member ("muting_notifications"); - relationship.requested = obj.get_boolean_member ("requested"); - relationship.domain_blocking = obj.get_boolean_member ("domain_blocking"); - return relationship; + public Relationship (Json.Object obj) { + Object ( + id: int64.parse (obj.get_string_member ("id")), + following: obj.get_boolean_member ("following"), + followed_by: obj.get_boolean_member ("followed_by"), + blocking: obj.get_boolean_member ("blocking"), + muting: obj.get_boolean_member ("muting"), + muting_notifications: obj.get_boolean_member ("muting_notifications"), + requested: obj.get_boolean_member ("requested"), + domain_blocking: obj.get_boolean_member ("domain_blocking") + ); } } diff --git a/src/API/Status.vala b/src/API/Status.vala index a24ca27..07f0e5c 100644 --- a/src/API/Status.vala +++ b/src/API/Status.vala @@ -1,91 +1,117 @@ -public class Tootle.API.Status { +using Gee; - public signal void updated (); +public class Tootle.API.Status : GLib.Object { - public API.Account account; - public int64 id; - public string uri; - public string url; - public string? spoiler_text; - public string content; - public int64 replies_count; - public int64 reblogs_count; - public int64 favourites_count; - public string created_at; - public bool reblogged = false; - public bool favorited = false; - public bool sensitive = false; - public bool muted = false; - public bool pinned = false; - public API.StatusVisibility visibility; - public API.Status? reblog; - public API.Mention[]? mentions; - public API.Attachment[]? attachments; - public Status (int64 _id) { - id = _id; + public int64 id { get; construct set; } + public API.Account account { get; construct set; } + public string uri { get; set; } + public string? url { get; set; default = null; } + public string? spoiler_text { get; set; default = null; } + public string? in_reply_to_id { get; set; default = null; } + public string? in_reply_to_account_id { get; set; default = null; } + public string content { get; set; default = ""; } + public int64 replies_count { get; set; default = 0; } + public int64 reblogs_count { get; set; default = 0; } + public int64 favourites_count { get; set; default = 0; } + public string created_at { get; set; default = "0"; } + public bool reblogged { get; set; default = false; } + public bool favorited { get; set; default = false; } + public bool sensitive { get; set; default = false; } + public bool muted { get; set; default = false; } + public bool pinned { get; set; default = false; } + public API.Visibility visibility { get; set; default = API.Visibility.PUBLIC; } + public API.Status? reblog { get; set; default = null; } + public ArrayList? mentions { get; set; default = null; } + public ArrayList? attachments { get; set; default = null; } + + public Status formal { + get { return reblog ?? this; } } - public Status get_formal () { - return reblog != null ? reblog : this; - } + public bool has_spoiler { + get { + return formal.spoiler_text != null || formal.sensitive; + } + } - public static Status parse (Json.Object obj) { - var id = int64.parse (obj.get_string_member ("id")); - var status = new Status (id); + public Status (Json.Object obj) { + Object ( + id: int64.parse (obj.get_string_member ("id")), + account: new Account (obj.get_object_member ("account")), + uri: obj.get_string_member ("uri"), + created_at: obj.get_string_member ("created_at"), + content: Html.simplify ( obj.get_string_member ("content")), + sensitive: obj.get_boolean_member ("sensitive"), + visibility: Visibility.from_string (obj.get_string_member ("visibility")), - status.account = Account.parse (obj.get_object_member ("account")); - status.uri = obj.get_string_member ("uri"); - status.created_at = obj.get_string_member ("created_at"); - status.replies_count = obj.get_int_member ("replies_count"); - status.reblogs_count = obj.get_int_member ("reblogs_count"); - status.favourites_count = obj.get_int_member ("favourites_count"); - status.content = Html.simplify ( obj.get_string_member ("content")); - status.sensitive = obj.get_boolean_member ("sensitive"); - status.visibility = StatusVisibility.from_string (obj.get_string_member ("visibility")); + in_reply_to_id: obj.get_string_member ("in_reply_to_id") ?? null, + in_reply_to_account_id: obj.get_string_member ("in_reply_to_account_id") ?? null, + + replies_count: obj.get_int_member ("replies_count"), + reblogs_count: obj.get_int_member ("reblogs_count"), + favourites_count: obj.get_int_member ("favourites_count") + ); if (obj.has_member ("url")) - status.url = obj.get_string_member ("url"); + url = obj.get_string_member ("url"); else - status.url = obj.get_string_member ("uri").replace ("/activity", ""); + url = obj.get_string_member ("uri").replace ("/activity", ""); var spoiler = obj.get_string_member ("spoiler_text"); if (spoiler != "") - status.spoiler_text = Html.simplify (spoiler); + spoiler_text = Html.simplify (spoiler); if (obj.has_member ("reblogged")) - status.reblogged = obj.get_boolean_member ("reblogged"); + reblogged = obj.get_boolean_member ("reblogged"); if (obj.has_member ("favourited")) - status.favorited = obj.get_boolean_member ("favourited"); + favorited = obj.get_boolean_member ("favourited"); if (obj.has_member ("muted")) - status.muted = obj.get_boolean_member ("muted"); + muted = obj.get_boolean_member ("muted"); if (obj.has_member ("pinned")) - status.pinned = obj.get_boolean_member ("pinned"); + pinned = obj.get_boolean_member ("pinned"); if (obj.has_member ("reblog") && obj.get_null_member("reblog") != true) - status.reblog = Status.parse (obj.get_object_member ("reblog")); + reblog = new Status (obj.get_object_member ("reblog")); - API.Mention[]? _mentions = {}; obj.get_array_member ("mentions").foreach_element ((array, i, node) => { - var object = node.get_object (); - if (object != null) - _mentions += API.Mention.parse (object); + var entity = node.get_object (); + if (entity != null) { + if (mentions == null) + mentions = new ArrayList (); + mentions.add (new API.Mention (entity)); + } }); - if (_mentions.length > 0) - status.mentions = _mentions; - API.Attachment[]? _attachments = {}; obj.get_array_member ("media_attachments").foreach_element ((array, i, node) => { - var object = node.get_object (); - if (object != null) - _attachments += API.Attachment.parse (object); + var entity = node.get_object (); + if (entity != null) { + if (attachments == null) + attachments = new ArrayList (); + attachments.add (new API.Attachment (entity)); + } }); - if (_attachments.length > 0) - status.attachments = _attachments; - - return status; } + public Status.empty () { + Object (id: -1); + } + + public Status.from_account (API.Account account) { + Object ( + id: 0, + account: account, + created_at: account.created_at + ); + + if (account.note == "") + content = ""; + else if ("\n" in account.note) + content = Html.remove_tags (account.note.split ("\n")[0]); + else + content = Html.remove_tags (account.note); + } + public Json.Node? serialize () { var builder = new Json.Builder (); builder.begin_object (); @@ -142,21 +168,17 @@ public class Tootle.API.Status { } public bool is_owned (){ - return get_formal ().account.id == accounts.current.id; - } - - public bool has_spoiler () { - return get_formal ().spoiler_text != null || get_formal ().sensitive; + return formal.account.id == accounts.active.id; } public string get_reply_mentions () { var result = ""; - if (account.acct != accounts.current.acct) + if (account.acct != accounts.active.acct) result = "@%s ".printf (account.acct); if (mentions != null) { foreach (var mention in mentions) { - var equals_current = mention.acct == accounts.current.acct; + var equals_current = mention.acct == accounts.active.acct; var already_mentioned = mention.acct in result; if (!equals_current && ! already_mentioned) @@ -167,69 +189,29 @@ public class Tootle.API.Status { return result; } - public void set_reblogged (bool rebl, Network.ErrorCallback? err = network.on_error) { - var action = rebl ? "reblog" : "unreblog"; - var msg = new Soup.Message ("POST", "%s/api/v1/statuses/%lld/%s".printf (accounts.formal.instance, id, action)); - msg.priority = Soup.MessagePriority.HIGH; - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, message) => { - reblogged = rebl; - updated (); - }, (status, reason) => { - err (status, reason); - }); + public void action (string action, owned Network.ErrorCallback? err = network.on_error) { + new Request.POST (@"/api/v1/statuses/$(formal.id)/$action") + .with_account (accounts.active) + .then_parse_obj (obj => { + var status = new API.Status (obj).formal; + formal.reblogged = status.reblogged; + formal.favorited = status.favorited; + formal.muted = status.muted; + formal.pinned = status.pinned; + }) + .on_error ((status, reason) => err (status, reason)) + .exec (); } - public void set_favorited (bool fav, Network.ErrorCallback? err = network.on_error) { - var action = fav ? "favourite" : "unfavourite"; - var msg = new Soup.Message ("POST", "%s/api/v1/statuses/%lld/%s".printf (accounts.formal.instance, id, action)); - msg.priority = Soup.MessagePriority.HIGH; - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, message) => { - favorited = fav; - updated (); - }, (status, reason) => { - err (status, reason); - }); - } - - public void set_muted (bool mute, Network.ErrorCallback? err = network.on_error) { - var action = mute ? "mute" : "unmute"; - var msg = new Soup.Message ("POST", "%s/api/v1/statuses/%lld/%s".printf (accounts.formal.instance, id, action)); - msg.priority = Soup.MessagePriority.HIGH; - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, message) => { - muted = mute; - updated (); - }, (status, reason) => { - err (status, reason); - }); - } - - public void set_pinned (bool pin, Network.ErrorCallback? err = network.on_error) { - var action = pin ? "pin" : "unpin"; - var msg = new Soup.Message ("POST", "%s/api/v1/statuses/%lld/%s".printf (accounts.formal.instance, id, action)); - msg.priority = Soup.MessagePriority.HIGH; - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, message) => { - pinned = pin; - updated (); - }, (status, reason) => { - err (status, reason); - }); - } - - public void poof (Soup.SessionCallback? cb = null, Network.ErrorCallback? err = network.on_error) { - var msg = new Soup.Message ("DELETE", "%s/api/v1/statuses/%lld".printf (accounts.formal.instance, id)); - msg.priority = Soup.MessagePriority.HIGH; - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, message) => { - network.status_removed (id); - if (cb != null) - cb (sess, message); - }, (status, reason) => { - err (status, reason); - }); + public void poof (owned Soup.SessionCallback? cb = null, owned Network.ErrorCallback? err = network.on_error) { + new Request.DELETE (@"/api/v1/statuses/$id") + .with_account (accounts.active) + .then ((sess, msg) => { + streams.status_removed (id); + cb (sess, msg); + }) + .on_error ((status, reason) => err (status, reason)) + .exec (); } } diff --git a/src/API/Tag.vala b/src/API/Tag.vala index 3e0fe91..bc11769 100644 --- a/src/API/Tag.vala +++ b/src/API/Tag.vala @@ -1,17 +1,13 @@ -public class Tootle.API.Tag{ +public class Tootle.API.Tag : GLib.Object { - public string name; - public string url; + public string name { get; construct set; } + public string url { get; construct set; } - public Tag (string _name, string _url) { - name = _name; - url = _url; - } - - public static Tag parse (Json.Object obj) { - var name = obj.get_string_member ("name"); - var url = obj.get_string_member ("url"); - return new Tag (name, url); + public Tag (Json.Object obj) { + Object ( + name: obj.get_string_member ("name"), + url: obj.get_string_member ("url") + ); } } diff --git a/src/API/StatusVisibility.vala b/src/API/Visibility.vala similarity index 59% rename from src/API/StatusVisibility.vala rename to src/API/Visibility.vala index 1da3f1d..1617194 100644 --- a/src/API/StatusVisibility.vala +++ b/src/API/Visibility.vala @@ -1,4 +1,4 @@ -public enum Tootle.API.StatusVisibility { +public enum Tootle.API.Visibility { PUBLIC, UNLISTED, PRIVATE, @@ -6,8 +6,6 @@ public enum Tootle.API.StatusVisibility { public string to_string () { switch (this) { - case PUBLIC: - return "public"; case UNLISTED: return "unlisted"; case PRIVATE: @@ -15,29 +13,38 @@ public enum Tootle.API.StatusVisibility { case DIRECT: return "direct"; default: - assert_not_reached(); + return "public"; } } - public static StatusVisibility from_string (string str) { + public static Visibility from_string (string str) { switch (str) { - case "public": - return StatusVisibility.PUBLIC; case "unlisted": - return StatusVisibility.UNLISTED; + return Visibility.UNLISTED; case "private": - return StatusVisibility.PRIVATE; + return Visibility.PRIVATE; case "direct": - return StatusVisibility.DIRECT; + return Visibility.DIRECT; default: - assert_not_reached(); + return Visibility.PUBLIC; + } + } + + public string get_name () { + switch (this) { + case UNLISTED: + return _("Unlisted"); + case PRIVATE: + return _("Followers-only"); + case DIRECT: + return _("Direct"); + default: + return _("Public"); } } public string get_desc () { switch (this) { - case PUBLIC: - return _("Post to public timelines"); case UNLISTED: return _("Don\'t post to public timelines"); case PRIVATE: @@ -45,27 +52,25 @@ public enum Tootle.API.StatusVisibility { case DIRECT: return _("Post to mentioned users only"); default: - assert_not_reached(); + return _("Post to public timelines"); } } public string get_icon () { switch (this) { - case PUBLIC: - return "network-workgroup-symbolic"; case UNLISTED: - return "view-private-symbolic"; + return "changes-allow-symbolic"; case PRIVATE: - return "security-medium-symbolic"; + return "changes-prevent-symbolic"; case DIRECT: return "user-available-symbolic"; default: - assert_not_reached(); + return "network-workgroup-symbolic"; } } - public static StatusVisibility[] get_all () { - return {StatusVisibility.PUBLIC, StatusVisibility.UNLISTED, StatusVisibility.PRIVATE, StatusVisibility.DIRECT}; + public static Visibility[] all () { + return {Visibility.PUBLIC, Visibility.UNLISTED, Visibility.PRIVATE, Visibility.DIRECT}; } } diff --git a/src/Accounts.vala b/src/Accounts.vala deleted file mode 100644 index 0294619..0000000 --- a/src/Accounts.vala +++ /dev/null @@ -1,144 +0,0 @@ -using GLib; - -public class Tootle.Accounts : Object { - - private string dir_path; - private string file_path; - - public signal void switched (API.Account? account); - public signal void updated (GenericArray accounts); - - public GenericArray saved_accounts = new GenericArray (); - public InstanceAccount? formal {get; set;} - public API.Account? current {get; set;} - - public Accounts () { - dir_path = "%s/%s".printf (GLib.Environment.get_user_config_dir (), app.application_id); - file_path = "%s/%s".printf (dir_path, "accounts.json"); - } - - public void switch_account (int id) { - info ("Switching to #%i", id); - settings.current_account = id; - formal = saved_accounts.@get (id); - var msg = new Soup.Message ("GET", "%s/api/v1/accounts/verify_credentials".printf (accounts.formal.instance)); - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, mess) => { - var root = network.parse (mess); - current = API.Account.parse (root); - switched (current); - updated (saved_accounts); - }, - network.on_show_error); - } - - public void add (InstanceAccount account) { - info ("Adding account for %s at %s", account.username, account.instance); - saved_accounts.add (account); - save (); - updated (saved_accounts); - switch_account (saved_accounts.length - 1); - account.start_notificator (); - } - - public void remove (int i) { - var account = saved_accounts.@get (i); - account.close_notificator (); - - saved_accounts.remove_index (i); - if (saved_accounts.length < 1) - switched (null); - else { - var id = settings.current_account - 1; - if (id > saved_accounts.length - 1) - id = saved_accounts.length - 1; - else if (id < saved_accounts.length - 1) - id = 0; - switch_account (id); - } - save (); - updated (saved_accounts); - - if (is_empty ()) { - window.destroy (); - Dialogs.NewAccount.open (); - } - } - - public bool is_empty () { - return saved_accounts.length == 0; - } - - public void init () { - save (false); - load (); - - if (saved_accounts.length < 1) - Dialogs.NewAccount.open (); - else - switch_account (settings.current_account); - } - - public void save (bool overwrite = true) { - try { - var dir = File.new_for_path (dir_path); - if (!dir.query_exists ()) - dir.make_directory (); - - var file = File.new_for_path (file_path); - if (file.query_exists () && !overwrite) - return; - - var builder = new Json.Builder (); - builder.begin_array (); - saved_accounts.foreach ((acc) => { - var node = acc.serialize (); - builder.add_value (node); - }); - builder.end_array (); - - var generator = new Json.Generator (); - generator.set_root (builder.get_root ()); - var data = generator.to_data (null); - - if (file.query_exists ()) - file.@delete (); - - FileOutputStream stream = file.create (FileCreateFlags.PRIVATE); - stream.write (data.data); - } - catch (GLib.Error e){ - warning (e.message); - } - } - - private void load () { - try { - uint8[] data; - string etag; - var file = File.new_for_path (file_path); - file.load_contents (null, out data, out etag); - var contents = (string) data; - - var parser = new Json.Parser (); - parser.load_from_data (contents, -1); - var array = parser.get_root ().get_array (); - - saved_accounts = new GenericArray (); - array.foreach_element ((_arr, _i, node) => { - var obj = node.get_object (); - var account = InstanceAccount.parse (obj); - if (account != null) { - saved_accounts.add (account); - account.start_notificator (); - } - }); - debug ("Loaded %i saved accounts", saved_accounts.length); - updated (saved_accounts); - } - catch (GLib.Error e){ - warning (e.message); - } - } - -} diff --git a/src/Application.vala b/src/Application.vala index 0cfe75d..db83dc7 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -3,6 +3,12 @@ using Granite; namespace Tootle { + public errordomain Oopsie { + USER, + PARSING, + INSTANCE + } + public static Application app; public static Dialogs.MainWindow? window; public static Window window_dummy; @@ -10,16 +16,23 @@ namespace Tootle { public static Settings settings; public static Accounts accounts; public static Network network; - public static ImageCache image_cache; - public static Watchlist watchlist; + public static Cache cache; + public static Streams streams; public static bool start_hidden = false; public class Application : Granite.Application { - public abstract signal void refresh (); - public abstract signal void toast (string title); - public abstract signal void error (string title, string text); + // These are used for the GTK Inspector + public Settings app_settings { get {return Tootle.settings; } } + public Accounts app_accounts { get {return Tootle.accounts; } } + public Network app_network { get {return Tootle.network; } } + public Cache app_cache { get {return Tootle.cache; } } + public Streams app_streams { get {return Tootle.streams; } } + + public signal void refresh (); + public signal void toast (string title); + public signal void error (string title, string text); public const GLib.OptionEntry[] app_options = { { "hidden", 0, 0, OptionArg.NONE, ref start_hidden, "Do not show main window on start", null }, @@ -27,22 +40,20 @@ namespace Tootle { }; public const GLib.ActionEntry[] app_entries = { - {"compose-toot", compose_toot_activated }, - {"toggle-reveal", on_sensitive_toggled }, + {"compose", compose_activated }, {"back", back_activated }, {"refresh", refresh_activated }, {"switch-timeline", switch_timeline_activated, "i" } }; construct { - application_id = "com.github.bleakgrey.tootle"; + application_id = Build.DOMAIN; flags = ApplicationFlags.FLAGS_NONE; - program_name = "Tootle"; - build_version = "0.2.0"; + program_name = Build.NAME; + build_version = Build.VERSION; } public string[] ACCEL_NEW_POST = {"T"}; - public string[] ACCEL_TOGGLE_REVEAL = {"S"}; public string[] ACCEL_BACK = {"BackSpace", "Left"}; public string[] ACCEL_REFRESH = {"R", "F5"}; public string[] ACCEL_TIMELINE_0 = {"1"}; @@ -52,6 +63,9 @@ namespace Tootle { public static int main (string[] args) { Gtk.init (ref args); + + Stacktrace.register_handlers (); + //assert (true == false); // I'm not crazy. It's for stacktrace testing. try { var opt_context = new OptionContext ("- Options"); @@ -71,10 +85,10 @@ namespace Tootle { Granite.Services.Logger.DisplayLevel = Granite.Services.LogLevel.INFO; settings = new Settings (); + streams = new Streams (); accounts = new Accounts (); network = new Network (); - image_cache = new ImageCache (); - watchlist = new Watchlist (); + cache = new Cache (); accounts.init (); app.error.connect (app.on_error); @@ -82,8 +96,7 @@ namespace Tootle { window_dummy = new Window (); add_window (window_dummy); - set_accels_for_action ("app.compose-toot", ACCEL_NEW_POST); - set_accels_for_action ("app.toggle-reveal", ACCEL_TOGGLE_REVEAL); + set_accels_for_action ("app.compose", ACCEL_NEW_POST); set_accels_for_action ("app.back", ACCEL_BACK); set_accels_for_action ("app.refresh", ACCEL_REFRESH); set_accels_for_action ("app.switch-timeline(0)", ACCEL_TIMELINE_0); @@ -104,13 +117,9 @@ namespace Tootle { return; } - debug ("Creating new window"); - if (accounts.is_empty ()) - Dialogs.NewAccount.open (); - else { - window = new Dialogs.MainWindow (this); - window.present (); - } + info ("Creating new window"); + window = new Dialogs.MainWindow (this); + window.present (); } protected void on_error (string title, string msg){ @@ -120,12 +129,8 @@ namespace Tootle { message_dialog.destroy (); } - private void on_sensitive_toggled () { - window.button_reveal.clicked (); - } - - private void compose_toot_activated () { - Dialogs.Compose.open (); + private void compose_activated () { + new Dialogs.Compose (); } private void back_activated () { diff --git a/src/Build.vala b/src/Build.vala new file mode 100644 index 0000000..eb90870 --- /dev/null +++ b/src/Build.vala @@ -0,0 +1,9 @@ +public class Build { + + public const string NAME = "Tootle"; + public const string WEBSITE = "https://github.com/bleakgrey/tootle"; + public const string DOMAIN = "com.github.bleakgrey.tootle"; + public const string RESOURCES = "/com/github/bleakgrey/tootle/"; + public const string VERSION = "1.0.0"; + +} diff --git a/src/Desktop.vala b/src/Desktop.vala index c026329..466cbed 100644 --- a/src/Desktop.vala +++ b/src/Desktop.vala @@ -30,37 +30,42 @@ public class Tootle.Desktop { } // Download a file from the web to a user's configured Downloads folder - public static void download_file (string url) { - debug ("Downloading file: %s", url); + public delegate void DownloadCallback (string path); + public static void download (string url, DownloadCallback? cb = null, Network.ErrorCallback? ecb = null) { + info (@"Downloading file: $url..."); var i = url.last_index_of ("/"); var name = url.substring (i + 1, url.length - i - 1); if (name == null) - name = "unknown"; + name = _("Unknown Attachment"); - var dir_path = "%s/%s".printf (GLib.Environment.get_user_special_dir (UserDirectory.DOWNLOAD), app.program_name); - var file_path = "%s/%s".printf (dir_path, name); + var downloads = GLib.Environment.get_user_special_dir (UserDirectory.DOWNLOAD); + var dir_path = @"$downloads/$(Build.NAME)"; + var file_path = @"$dir_path/$name"; - var msg = new Soup.Message("GET", url); - msg.finished.connect(() => { - try { - var dir = File.new_for_path (dir_path); - if (!dir.query_exists ()) - dir.make_directory (); + new Request.GET (url) + .then ((sess, msg) => { + try { + var dir = File.new_for_path (dir_path); + if (!dir.query_exists ()) + dir.make_directory (); - var file = File.new_for_path (file_path); - if (!file.query_exists ()) { - var data = msg.response_body.data; - FileOutputStream stream = file.create (FileCreateFlags.PRIVATE); - stream.write (data); + var file = File.new_for_path (file_path); + if (!file.query_exists ()) { + var data = msg.response_body.data; + FileOutputStream stream = file.create (FileCreateFlags.PRIVATE); + stream.write (data); + } + info ("OK"); + cb (file_path); + + } catch (Error e) { + warning ("Error: %s\n", e.message); + ecb (0, e.message); } - app.toast (_("Media downloaded")); - } catch (Error e) { - app.toast (e.message); - warning ("Error: %s\n", e.message); - } - }); - network.queue (msg); + }) + .on_error ((code, reason) => ecb) + .exec (); } public static string fallback_icon (string normal, string fallback) { diff --git a/src/Dialogs/Compose.vala b/src/Dialogs/Compose.vala index a928495..a56018f 100644 --- a/src/Dialogs/Compose.vala +++ b/src/Dialogs/Compose.vala @@ -1,241 +1,145 @@ using Gtk; -public class Tootle.Dialogs.Compose : Dialog { +[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/dialogs/compose.ui")] +public class Tootle.Dialogs.Compose : Window { - private static Compose dialog; + public API.Status? status { get; construct set; } + public string style_class { get; construct set; } + public string label { get; construct set; } + public int char_limit { + get { + return 250; + } + } - protected TextView text; - private ScrolledWindow scroll; - private Label counter; - private Widgets.ImageToggleButton spoiler; - private MenuButton visibility; - private Button attach; - private Button cancel; - private Button publish; - protected Widgets.AttachmentGrid attachments; - private Revealer spoiler_revealer; - private Entry spoiler_text; + [GtkChild] + protected Box box; - protected API.Status? replying_to; - protected API.Status? redrafting; - protected API.StatusVisibility visibility_opt = API.StatusVisibility.PUBLIC; - protected int char_limit; + [GtkChild] + protected Revealer cw_revealer; + [GtkChild] + protected ToggleButton cw_button; + [GtkChild] + protected Entry cw; + [GtkChild] + protected Label counter; - public Compose (API.Status? _replying_to = null, API.Status? _redrafting = null) { - border_width = 6; - deletable = false; - resizable = true; - title = _("Toot"); + [GtkChild] + protected MenuButton visibility_button; + [GtkChild] + protected Image visibility_icon; + protected Widgets.VisibilityPopover visibility_popover; + [GtkChild] + protected Button post_button; + + [GtkChild] + protected TextView content; + + construct { transient_for = window; - char_limit = settings.char_limit; - replying_to = _replying_to; - redrafting = _redrafting; - if (replying_to != null) - visibility_opt = replying_to.visibility; - if (redrafting != null) - visibility_opt = redrafting.visibility; + post_button.label = label; + foreach (Widget w in new Widget[] { visibility_button, post_button }) + w.get_style_context ().add_class (style_class); - var actions = get_action_area ().get_parent () as Box; - var content = get_content_area (); - get_action_area ().hexpand = false; + visibility_popover = new Widgets.VisibilityPopover.with_button (visibility_button); + visibility_popover.bind_property ("selected", visibility_icon, "icon-name", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + target.set_string (((API.Visibility)src).get_icon ()); + return true; + }); - visibility = get_visibility_btn (); - visibility.tooltip_text = _("Post Visibility"); - visibility.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - visibility.get_style_context ().remove_class ("image-button"); - visibility.can_default = false; - (visibility as Widget).set_focus_on_click (false); + cw_button.bind_property ("active", cw_revealer, "reveal_child", BindingFlags.SYNC_CREATE); - attach = new Button.from_icon_name ("mail-attachment-symbolic"); - attach.tooltip_text = _("Add Media"); - attach.valign = Align.CENTER; - attach.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - attach.get_style_context ().remove_class ("image-button"); - attach.can_default = false; - (attach as Widget).set_focus_on_click (false); - attach.clicked.connect (() => attachments.select ()); + cw_button.toggled.connect (validate); + cw.buffer.deleted_text.connect (() => validate ()); + cw.buffer.inserted_text.connect (() => validate ()); + content.buffer.changed.connect (validate); + post_button.clicked.connect (on_post_button_clicked); - spoiler = new Widgets.ImageToggleButton ("image-red-eye-symbolic"); - spoiler.tooltip_text = _("Spoiler Warning"); - spoiler.set_action (); - spoiler.toggled.connect (() => { - spoiler_revealer.reveal_child = spoiler.active; - validate (); - }); - - cancel = add_button (_("Cancel"), 5) as Button; - cancel.clicked.connect(() => destroy ()); - - if (redrafting != null) { - publish = add_button (_("Redraft"), 5) as Button; - publish.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - publish.clicked.connect (redraft_post); - } - else { - publish = add_button (_("Toot!"), 5) as Button; - publish.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - publish.clicked.connect (publish_post); + if (status.spoiler_text != null) { + cw.text = status.spoiler_text; + cw_button.active = true; } + content.buffer.text = Html.remove_tags (status.content); - spoiler_text = new Entry (); - spoiler_text.margin_start = 6; - spoiler_text.margin_end = 6; - spoiler_text.placeholder_text = _("Write your warning here"); - spoiler_text.changed.connect (validate); + show (); + } - spoiler_revealer = new Revealer (); - spoiler_revealer.add (spoiler_text); + public Compose () { + Object (status: new API.Status.empty (), style_class: STYLE_CLASS_SUGGESTED_ACTION, label: _("Post")); + } - text = new TextView (); - text.get_style_context ().add_class ("toot-text"); - text.wrap_mode = WrapMode.WORD; - text.accepts_tab = false; - text.vexpand = true; - text.buffer.changed.connect (validate); + public Compose.redraft (API.Status status) { + Object (status: status, style_class: STYLE_CLASS_DESTRUCTIVE_ACTION, label: _("Redraft")); + } - scroll = new ScrolledWindow (null, null); - scroll.hscrollbar_policy = PolicyType.NEVER; - scroll.min_content_height = 120; - scroll.vexpand = true; - scroll.propagate_natural_height = true; - scroll.margin_start = 6; - scroll.margin_end = 6; - scroll.add (text); - scroll.show_all (); + public Compose.reply (API.Status status) { + var template = new API.Status.empty (); + template.in_reply_to_id = status.in_reply_to_id; + template.in_reply_to_account_id = status.in_reply_to_account_id; + template.content = status.formal.get_reply_mentions (); + Object (status: template, style_class: STYLE_CLASS_SUGGESTED_ACTION, label: _("Reply")); + visibility_popover.selected = status.visibility; + } - attachments = new Widgets.AttachmentGrid (true); - counter = new Label (""); + protected void validate () { + var remain = char_limit - content.buffer.get_char_count (); + if (cw_button.active) + remain -= (int)cw.buffer.length; - actions.pack_start (counter, false, false, 6); - actions.pack_end (spoiler, false, false, 6); - actions.pack_end (visibility, false, false, 0); - actions.pack_end (attach, false, false, 6); - content.pack_start (spoiler_revealer, false, false, 6); - content.pack_start (scroll, false, false, 6); - content.pack_start (attachments, false, false, 6); - content.set_size_request (350, 120); + counter.label = remain.to_string (); + post_button.sensitive = remain >= 0; + visibility_button.sensitive = true; + box.sensitive = true; + } - if (replying_to != null) { - spoiler.active = replying_to.sensitive; - var status_spoiler_text = replying_to.spoiler_text != null ? replying_to.spoiler_text : ""; - spoiler_text.set_text (status_spoiler_text); - } - if (redrafting != null) { - spoiler.active = redrafting.sensitive; - var status_spoiler_text = redrafting.spoiler_text != null ? redrafting.spoiler_text : ""; - spoiler_text.set_text (status_spoiler_text); - } - - destroy.connect (() => dialog = null); - - show_all (); - attachments.hide (); - text.grab_focus (); + protected void on_error (int32 code, string reason) { //TODO: display errors + warning (reason); validate (); } - private MenuButton get_visibility_btn () { - var button = new MenuButton (); - var menu = new Popover (null); - var box = new Box (Orientation.VERTICAL, 6); - box.margin = 12; - menu.add (box); - button.direction = ArrowType.DOWN; - button.image = new Image.from_icon_name (visibility_opt.get_icon (), IconSize.BUTTON); + protected void on_post_button_clicked () { + post_button.sensitive = false; + visibility_button.sensitive = false; + box.sensitive = false; - RadioButton? first = null; - foreach (API.StatusVisibility opt in API.StatusVisibility.get_all ()){ - var item = new RadioButton.with_label_from_widget (first, opt.get_desc ()); - if (first == null) - first = item; + if (status.id >= 0) { + info ("Removing old status..."); + status.poof (publish, on_error); + } + else { + publish (); + } + } - item.toggled.connect (() => { - visibility_opt = opt; - (button.image as Image).icon_name = visibility_opt.get_icon (); - }); - item.active = visibility_opt == opt; - box.pack_start (item, false, false, 0); + protected void publish () { + info ("Publishing new status..."); + status.content = content.buffer.text; + status.spoiler_text = cw.text; + + var req = new Request.POST ("/api/v1/statuses") + .with_account () + .with_param ("visibility", visibility_popover.selected.to_string ()) + .with_param ("status", Html.uri_encode (status.content)); + + if (cw_button.active) { + req.with_param ("sensitive", "true"); + req.with_param ("spoiler_text", Html.uri_encode (cw.text)); } - box.show_all (); - button.use_popover = true; - button.popover = menu; - button.valign = Align.CENTER; - button.show (); - return button; - } + if (status.in_reply_to_id != null) + req.with_param ("in_reply_to_id", status.in_reply_to_id); + if (status.in_reply_to_account_id != null) + req.with_param ("in_reply_to_account_id", status.in_reply_to_account_id); - private void validate () { - var remain = char_limit - text.buffer.get_char_count (); - if (spoiler.active) - remain -= (int)spoiler_text.buffer.length; - - counter.label = remain.to_string (); - publish.sensitive = remain >= 0; - } - - public static void open (string? text = null, API.Status? reply_to = null) { - if (dialog == null){ - dialog = new Compose (reply_to); - - if (text != null) - dialog.text.buffer.text = text; - } - else if (text != null) - dialog.text.buffer.text += text; - } - - public static void reply (API.Status status) { - if (dialog != null) - return; - - open (null, status); - dialog.text.buffer.text = status.get_reply_mentions (); - } - - public static void redraft (API.Status status) { - if (dialog != null) - return; - dialog = new Compose (null, status); - - if (status.attachments != null) { - foreach (API.Attachment attachment in status.attachments) - dialog.attachments.append (attachment); - } - - var content = Html.simplify (status.content); - content = Html.remove_tags (content); - content = Widgets.RichLabel.restore_entities (content); - dialog.text.buffer.text = content; - } - - private void publish_post () { - var pars = "?status=%s&visibility=%s".printf (Html.uri_encode (text.buffer.text), visibility_opt.to_string ()); - pars += attachments.get_uri_array (); - if (replying_to != null) - pars += "&in_reply_to_id=%s".printf (replying_to.id.to_string ()); - - if (spoiler.active) { - pars += "&sensitive=true"; - pars += "&spoiler_text=" + Html.uri_encode (spoiler_text.buffer.text); - } - - var url = "%s/api/v1/statuses%s".printf (accounts.formal.instance, pars); - var msg = new Soup.Message ("POST", url); - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, mess) => { + req.then ((sess, mess) => { var root = network.parse (mess); - var status = API.Status.parse (root); - debug ("Posted: %s", status.id.to_string ()); //TODO: Live updates + var status = new API.Status (root); + info ("OK: status id is %s", status.id.to_string ()); destroy (); - }); - } - - private void redraft_post () { - redrafting.poof ((sess, msg) => { - publish_post (); - }); + }) + .on_error (on_error) + .exec (); } } diff --git a/src/Dialogs/MainWindow.vala b/src/Dialogs/MainWindow.vala index fc508e4..a7c905c 100644 --- a/src/Dialogs/MainWindow.vala +++ b/src/Dialogs/MainWindow.vala @@ -1,122 +1,55 @@ using Gtk; using Gdk; +[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/dialogs/main.ui")] public class Tootle.Dialogs.MainWindow: Gtk.Window, ISavedWindow { - private Overlay overlay; - public Granite.Widgets.Toast toast; - private Grid grid; - private Stack view_stack; - private Stack timeline_stack; + [GtkChild] + protected Stack view_stack; + [GtkChild] + protected Stack timeline_stack; - public HeaderBar header; - public Granite.Widgets.ModeButton button_mode; - private Widgets.AccountsButton button_accounts; - private Spinner spinner; - private Button button_toot; - private Button button_back; - public Button button_reveal; - - public Views.Home home = new Views.Home (); - public Views.Notifications notifications = new Views.Notifications (); - public Views.Local local = new Views.Local (); - public Views.Federated federated = new Views.Federated (); + [GtkChild] + protected HeaderBar header; + [GtkChild] + protected Button back_button; + [GtkChild] + protected Button compose_button; + [GtkChild] + protected Granite.Widgets.ModeButton timeline_switcher; + [GtkChild] + protected Widgets.AccountsButton accounts_button; construct { - var provider = new Gtk.CssProvider (); - provider.load_from_resource ("/com/github/bleakgrey/tootle/app.css"); - StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + provider.load_from_resource (@"$(Build.RESOURCES)app.css"); + StyleContext.add_provider_for_screen (Screen.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + back_button.clicked.connect (() => back ()); + Desktop.set_hotkey_tooltip (back_button, _("Back"), app.ACCEL_BACK); + + compose_button.clicked.connect (() => new Dialogs.Compose ()); + Desktop.set_hotkey_tooltip (compose_button, _("Compose"), app.ACCEL_NEW_POST); + + timeline_switcher.mode_changed.connect (on_mode_changed); + + add_header_view (new Views.Home (), app.ACCEL_TIMELINE_0, 0); + add_header_view (new Views.Notifications (), app.ACCEL_TIMELINE_1, 1); + add_header_view (new Views.Local (), app.ACCEL_TIMELINE_2, 2); + add_header_view (new Views.Federated (), app.ACCEL_TIMELINE_3, 3); + timeline_switcher.set_active (0); + + button_press_event.connect (on_button_press); settings.changed.connect (update_theme); update_theme (); - - timeline_stack = new Stack(); - timeline_stack.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT; - timeline_stack.show (); - view_stack = new Stack(); - view_stack.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT; - view_stack.show (); - view_stack.add_named (timeline_stack, "0"); - view_stack.hexpand = view_stack.vexpand = true; - - spinner = new Spinner (); - spinner.active = true; - - button_accounts = new Widgets.AccountsButton (); - - button_back = new Button (); - button_back.valign = Align.CENTER; - button_back.label = _("Back"); - button_back.get_style_context ().add_class (Granite.STYLE_CLASS_BACK_BUTTON); - button_back.clicked.connect (() => back ()); - Desktop.set_hotkey_tooltip (button_back, null, app.ACCEL_BACK); - - button_toot = new Button (); - button_toot.valign = Align.CENTER; - button_toot.image = new Image.from_icon_name ("document-edit-symbolic", IconSize.LARGE_TOOLBAR); - button_toot.clicked.connect (() => Dialogs.Compose.open ()); - Desktop.set_hotkey_tooltip (button_toot, _("Toot"), app.ACCEL_NEW_POST); - - button_reveal = new Button (); - button_reveal.valign = Align.CENTER; - button_reveal.image = new Image.from_icon_name ("image-red-eye-symbolic", IconSize.LARGE_TOOLBAR); - Desktop.set_hotkey_tooltip (button_reveal, _("Toggle content"), app.ACCEL_TOGGLE_REVEAL); - - button_mode = new Granite.Widgets.ModeButton (); - button_mode.get_style_context ().add_class ("mode"); - button_mode.vexpand = true; - button_mode.valign = Align.FILL; - button_mode.mode_changed.connect (on_mode_changed); - button_mode.show (); - - header = new HeaderBar (); - header.get_style_context ().add_class ("compact"); - header.show_close_button = true; - header.title = _("Tootle"); - header.custom_title = button_mode; - header.pack_start (button_back); - header.pack_start (button_toot); - header.pack_end (button_accounts); - header.pack_end (button_reveal); - header.pack_end (spinner); - header.show_all (); - - grid = new Grid (); - grid.attach (view_stack, 0, 0, 1, 1); - - add_header_view (home, app.ACCEL_TIMELINE_0, 0); - add_header_view (notifications, app.ACCEL_TIMELINE_1, 1); - add_header_view (local, app.ACCEL_TIMELINE_2, 2); - add_header_view (federated, app.ACCEL_TIMELINE_3, 3); - button_mode.set_active (0); - - toast = new Granite.Widgets.Toast (""); - overlay = new Overlay (); - overlay.add_overlay (grid); - overlay.add_overlay (toast); - overlay.set_size_request (450, 600); - add (overlay); - + update_header (); restore_state (); - show_all (); - - button_reveal.hide (); } - public MainWindow (Gtk.Application _app) { - application = _app; - icon_name = "com.github.bleakgrey.tootle"; - resizable = true; - window_position = WindowPosition.CENTER; - set_titlebar (header); - update_header (); - - app.toast.connect (on_toast); - network.started.connect (() => spinner.show ()); - network.finished.connect (() => spinner.hide ()); - accounts.updated (accounts.saved_accounts); - button_press_event.connect (on_button_press); + public MainWindow (Gtk.Application app) { + Object (application: app, icon_name: Build.DOMAIN, resizable: true, window_position: WindowPosition.CENTER); + if (accounts.is_empty ()) + open_view (new Views.NewAccount (false)); } private bool on_button_press (EventButton ev) { @@ -125,22 +58,19 @@ public class Tootle.Dialogs.MainWindow: Gtk.Window, ISavedWindow { return false; } - private void add_header_view (Views.Abstract view, string[] accelerators, int32 num) { + private void add_header_view (Views.Base view, string[] accelerators, int32 num) { var img = new Image.from_icon_name (view.get_icon (), IconSize.LARGE_TOOLBAR); Desktop.set_hotkey_tooltip (img, view.get_name (), accelerators); - button_mode.append (img); + timeline_switcher.append (img); view.image = img; timeline_stack.add_named (view, num.to_string ()); - - if (view is Views.Notifications) - img.pixel_size = 20; // For some reason Notifications icon is too small without this } public int get_visible_id () { return int.parse (view_stack.get_visible_child_name ()); } - public bool open_view (Views.Abstract widget) { + public bool open_view (Views.Base widget) { var i = get_visible_id (); i++; widget.stack_pos = i; @@ -171,7 +101,7 @@ public class Tootle.Dialogs.MainWindow: Gtk.Window, ISavedWindow { } } - public override bool delete_event (Gdk.EventAny event) { + public override bool delete_event (EventAny event) { destroy.connect (() => { if (!settings.always_online || accounts.is_empty ()) app.remove_window (window_dummy); @@ -181,39 +111,28 @@ public class Tootle.Dialogs.MainWindow: Gtk.Window, ISavedWindow { } public void switch_timeline (int32 timeline_no) { - button_mode.set_active (timeline_no); + timeline_switcher.set_active (timeline_no); } private void update_theme () { - var provider = new Gtk.CssProvider (); - var is_dark = settings.dark_theme; - var theme = is_dark ? "dark" : "light"; - provider.load_from_resource ("/com/github/bleakgrey/tootle/%s.css".printf (theme)); - StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - Gtk.Settings.get_default ().gtk_application_prefer_dark_theme = is_dark; + Gtk.Settings.get_default ().gtk_application_prefer_dark_theme = settings.dark_theme; } private void update_header () { bool primary_mode = get_visible_id () == 0; - button_mode.sensitive = primary_mode; - button_mode.opacity = primary_mode ? 1 : 0; //Prevent HeaderBar height jitter - button_toot.set_visible (primary_mode); - button_back.set_visible (!primary_mode); - button_accounts.set_visible (true); - } - - private void on_toast (string msg){ - toast.title = msg; - toast.send_notification (); + timeline_switcher.sensitive = primary_mode; + timeline_switcher.opacity = primary_mode ? 1 : 0; //Prevent HeaderBar height jitter + compose_button.visible = primary_mode; + back_button.visible = !primary_mode; } private void on_mode_changed (Widget widget) { - var visible = timeline_stack.get_visible_child () as Views.Abstract; + var visible = timeline_stack.get_visible_child () as Views.Base; visible.current = false; - timeline_stack.set_visible_child_name (button_mode.selected.to_string ()); + timeline_stack.set_visible_child_name (timeline_switcher.selected.to_string ()); - visible = timeline_stack.get_visible_child () as Views.Abstract; + visible = timeline_stack.get_visible_child () as Views.Base; visible.current = true; visible.on_set_current (); } diff --git a/src/Dialogs/NewAccount.vala b/src/Dialogs/NewAccount.vala deleted file mode 100644 index 43faf67..0000000 --- a/src/Dialogs/NewAccount.vala +++ /dev/null @@ -1,192 +0,0 @@ -using Gtk; - -public class Tootle.Dialogs.NewAccount : Dialog { - - private static NewAccount dialog; - - private Grid grid; - private Button button_done; - private Image logo; - private Entry instance_entry; - private Label instance_register; - private Label code_name; - private Entry code_entry; - - private string? instance; - private string? client_id; - private string? client_secret; - private string? code; - private string? token; - private string? username; - - public NewAccount () { - border_width = 6; - deletable = true; - resizable = false; - title = _("New Account"); - transient_for = window; - - logo = new Image.from_resource ("/com/github/bleakgrey/tootle/logo128"); - logo.halign = Align.CENTER; - logo.hexpand = true; - logo.margin_bottom = 24; - - instance_entry = new Entry (); - instance_entry.width_chars = 30; - - instance_register = new Label ("%s".printf (_("What's an instance?"))); - instance_register.halign = Align.END; - instance_register.set_use_markup (true); - - code_name = new Widgets.AlignedLabel (_("Code:")); - - code_entry = new Entry (); - code_entry.secondary_icon_name = "dialog-question-symbolic"; - code_entry.secondary_icon_tooltip_text = _("Paste your instance authorization code here"); - code_entry.secondary_icon_activatable = false; - - button_done = new Button.with_label (_("Add Account")); - button_done.clicked.connect (on_done_clicked); - button_done.halign = Align.END; - button_done.margin_top = 24; - - grid = new Grid (); - grid.column_spacing = 12; - grid.row_spacing = 6; - grid.hexpand = true; - grid.halign = Align.CENTER; - grid.attach (logo, 0, 0, 2, 1); - grid.attach (new Widgets.AlignedLabel (_("Instance:")), 0, 1); - grid.attach (instance_entry, 1, 1); - grid.attach (code_name, 0, 3); - grid.attach (code_entry, 1, 3); - grid.attach (instance_register, 1, 5); - grid.attach (button_done, 1, 10); - - var content = get_content_area () as Box; - content.pack_start (grid, false, false, 0); - - destroy.connect (() => { - dialog = null; - - if (accounts.is_empty ()) - app.remove_window (window_dummy); - }); - - show_all (); - clear (); - } - - private void clear () { - code_name.hide (); - code_entry.hide (); - code_entry.text = ""; - client_id = client_secret = code = token = null; - } - - private void on_done_clicked () { - instance = "https://" + instance_entry.text - .replace ("/", "") - .replace (":", "") - .replace ("https", "") - .replace ("http", ""); - code = code_entry.text; - - if (client_id == null || client_secret == null) { - request_client_tokens (); - return; - } - - if (code == "") - app.error (_("Error"), _("Please paste valid instance authorization code")); - else - try_auth (code); - } - - private void request_client_tokens (){ - var pars = "?client_name=Tootle"; - pars += "&redirect_uris=urn:ietf:wg:oauth:2.0:oob"; - pars += "&website=https://github.com/bleakgrey/tootle"; - pars += "&scopes=read%20write%20follow"; - - grid.sensitive = false; - var message = new Soup.Message ("POST", "%s/api/v1/apps%s".printf (instance, pars)); - network.queue (message, (sess, msg) => { - grid.sensitive = true; - - var root = network.parse (msg); - var id = root.get_string_member ("client_id"); - var secret = root.get_string_member ("client_secret"); - client_id = id; - client_secret = secret; - - info ("Received tokens from %s", instance); - request_auth_code (); - code_name.show (); - code_entry.show (); - }, (status, reason) => { - network.on_show_error (status, reason); - }); - } - - private void request_auth_code (){ - var pars = "?scope=read%20write%20follow"; - pars += "&response_type=code"; - pars += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob"; - pars += "&client_id=" + client_id; - - info ("Requesting auth token"); - Desktop.open_uri ("%s/oauth/authorize%s".printf (instance, pars)); - } - - private void try_auth (string code){ - var pars = "?client_id=" + client_id; - pars += "&client_secret=" + client_secret; - pars += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob"; - pars += "&grant_type=authorization_code"; - pars += "&code=" + code; - - var message = new Soup.Message ("POST", "%s/oauth/token%s".printf (instance, pars)); - network.queue (message, (sess, msg) => { - var root = network.parse (msg); - token = root.get_string_member ("access_token"); - - info ("Got access token"); - get_username (); - }, (status, reason) => { - network.on_show_error (status, reason); - }); - } - - private void get_username () { - var message = new Soup.Message("GET", "%s/api/v1/accounts/verify_credentials".printf (instance)); - message.request_headers.append ("Authorization", "Bearer " + token); - network.queue (message, (sess, msg) => { - var root = network.parse (msg); - username = root.get_string_member ("username"); - add_account (); - window.show (); - window.present (); - destroy (); - }, (status, reason) => { - network.on_show_error (status, reason); - }); - } - - private void add_account () { - var account = new InstanceAccount (); - account.username = username; - account.instance = instance; - account.client_id = client_id; - account.client_secret = client_secret; - account.token = token; - accounts.add (account); - app.activate (); - } - - public static void open () { - if (dialog == null) - dialog = new NewAccount (); - } - -} diff --git a/src/Dialogs/Preferences.vala b/src/Dialogs/Preferences.vala index f0646f9..b476a74 100644 --- a/src/Dialogs/Preferences.vala +++ b/src/Dialogs/Preferences.vala @@ -88,7 +88,7 @@ public class Tootle.Dialogs.Preferences : Dialog { halign = Align.START; valign = Align.CENTER; margin_bottom = 6; - settings.schema.bind (setting, this, "active", SettingsBindFlags.DEFAULT); + settings.bind (setting, this, "active", SettingsBindFlags.DEFAULT); } } diff --git a/src/Dialogs/WatchlistEditor.vala b/src/Dialogs/WatchlistEditor.vala deleted file mode 100644 index 29cf1aa..0000000 --- a/src/Dialogs/WatchlistEditor.vala +++ /dev/null @@ -1,206 +0,0 @@ -using Gtk; -using Gee; - -public class Tootle.Dialogs.WatchlistEditor : Dialog { - - private static WatchlistEditor dialog; - - private StackSwitcher switcher; - private MenuButton button_add; - private Button button_remove; - private Stack stack; - private ListStack users; - private ListStack hashtags; - private ActionBar actionbar; - private Popover popover; - private Grid popover_grid; - private Entry popover_entry; - private Button popover_button; - - private const string TIP_USERS = _("You'll be notified when toots from this user appear in your Home timeline."); - private const string TIP_HASHTAGS = _("You'll be notified when toots with this hashtag appear in any public timelines."); - - private class ModelItem : GLib.Object { - public string name; - - public ModelItem (string name) { - this.name = name; - } - } - - private class ModelView : ListBoxRow { - public Label label; - public ModelView (ModelItem item) { - label = new Label (item.name); - label.margin = 6; - label.halign = Align.START; - label.justify = Justification.LEFT; - add (label); - show_all (); - } - } - - private class Model : GLib.ListModel, GLib.Object { - private GenericArray items = new GenericArray (); - - public GLib.Type get_item_type () { - return typeof (ModelItem); - } - - public uint get_n_items () { - return items.length; - } - - public GLib.Object? get_item (uint position) { - return items.@get ((int)position); - } - - public void append (ModelItem item) { - this.items.add (item); - } - - } - - public static Widget create_row (GLib.Object obj) { - var item = (ModelItem) obj; - return new ModelView (item); - } - - private class ListStack : ScrolledWindow { - public Model model; - public ListBox list; - - public void update (ArrayList array) { - array.@foreach (item => { - model.append (new ModelItem (item)); - return true; - }); - list.bind_model (model, create_row); - } - - public ListStack (ArrayList array) { - model = new Model (); - list = new ListBox (); - add (list); - update (array); - } - } - - private void set_tip () { - var is_user = stack.visible_child_name == "users"; - popover_entry.secondary_icon_tooltip_text = is_user ? TIP_USERS : TIP_HASHTAGS; - } - - public WatchlistEditor () { - border_width = 6; - deletable = false; - resizable = false; - transient_for = window; - title = _("Watchlist"); - - users = new ListStack (watchlist.users); - hashtags = new ListStack (watchlist.hashtags); - - stack = new Stack (); - stack.transition_type = StackTransitionType.SLIDE_LEFT_RIGHT; - stack.hexpand = true; - stack.vexpand = true; - stack.add_titled (users, "users", _("Users")); - stack.add_titled (hashtags, "hashtags", _("Hashtags")); - - switcher = new StackSwitcher (); - switcher.stack = stack; - switcher.halign = Align.CENTER; - switcher.margin_bottom = 12; - - popover_entry = new Entry (); - popover_entry.hexpand = true; - popover_entry.secondary_icon_name = "dialog-information-symbolic"; - popover_entry.secondary_icon_activatable = false; - popover_entry.activate.connect (() => submit ()); - - popover_button = new Button.with_label (_("Add")); - popover_button.halign = Align.END; - popover_button.margin_start = 6; - popover_button.clicked.connect (() => submit ()); - - popover_grid = new Grid (); - popover_grid.margin = 6; - popover_grid.attach (popover_entry, 0, 0); - popover_grid.attach (popover_button, 1, 0); - popover_grid.show_all (); - - popover = new Popover (null); - popover.add (popover_grid); - - button_add = new MenuButton (); - button_add.image = new Image.from_icon_name ("list-add-symbolic", IconSize.BUTTON); - button_add.popover = popover; - button_add.clicked.connect (() => set_tip ()); - - button_remove = new Button (); - button_remove.image = new Image.from_icon_name ("list-remove-symbolic", IconSize.BUTTON); - button_remove.clicked.connect (on_remove); - - actionbar = new ActionBar (); - actionbar.add (button_add); - actionbar.add (button_remove); - - var grid = new Grid (); - grid.attach (stack, 0, 1); - grid.attach (actionbar, 0, 2); - - var frame = new Frame (null); - frame.margin_bottom = 6; - frame.add (grid); - frame.set_size_request (350, 350); - - var content = get_content_area (); - content.pack_start (switcher, true, true, 0); - content.pack_start (frame, true, true, 0); - - add_button (_("_Close"), ResponseType.DELETE_EVENT); - show_all (); - - response.connect (on_response); - destroy.connect (() => dialog = null); - } - - private void on_response (int i) { - destroy (); - } - - private void on_remove () { - var is_hashtag = stack.visible_child_name == "hashtags"; - ListStack stack = is_hashtag ? hashtags : users; - stack.list.get_selected_rows ().@foreach (_row => { - var row = _row as ModelView; - watchlist.remove (row.label.label, is_hashtag); - watchlist.save (); - row.destroy (); - }); - } - - private void submit () { - if (popover_entry.text_length < 1) - return; - - var is_hashtag = stack.visible_child_name == "hashtags"; - var entity = popover_entry.text - .replace ("#", "") - .replace (" ", ""); - - watchlist.add (entity, is_hashtag); - watchlist.save (); - button_add.active = false; - - var stack = is_hashtag ? hashtags : users; - stack.list.insert (create_row (new ModelItem (entity)), 0); - } - - public static void open () { - if (dialog == null) - dialog = new WatchlistEditor (); - } - -} diff --git a/src/Drawing.vala b/src/Drawing.vala index 6a70a76..1d293e5 100644 --- a/src/Drawing.vala +++ b/src/Drawing.vala @@ -13,23 +13,24 @@ public class Tootle.Drawing { ctx.close_path (); } - public static Pixbuf make_pixbuf_thumbnail (Pixbuf pixbuf, int view_w, int view_h, bool fill_parent = false) { - // Don't resize if parent view is bigger than actual image - if (view_w >= pixbuf.width && view_h >= pixbuf.height) - return pixbuf; + public static void center (Cairo.Context ctx, int w, int h, int tw, int th) { + var cx = w/2 - tw/2; + var cy = h/2 - th/2; + ctx.translate (cx, cy); + } - //Otherwise fit the image into the parent view - var resized_w = view_w; - var resized_h = view_h; - //resized_w = (pixbuf.width * view_h) / pixbuf.height; - //resized_h = (pixbuf.height * view_w) / pixbuf.width; + public static Pixbuf make_thumbnail (Pixbuf pb, int view_w, int view_h) { + if (view_w >= pb.width && view_h >= pb.height) + return pb; - if (fill_parent) - resized_h = (pixbuf.height * view_w) / pixbuf.width; - else - resized_w = (pixbuf.width * view_h) / pixbuf.height; + double ratio_x = (double) view_w / (double) pb.width; + double ratio_y = (double) view_h / (double) pb.height; + double ratio = ratio_x < ratio_y ? ratio_x : ratio_y; - return pixbuf.scale_simple (resized_w, resized_h, InterpType.BILINEAR); + return pb.scale_simple ( + (int) (pb.width * ratio), + (int) (pb.height * ratio), + InterpType.BILINEAR); } } diff --git a/src/Html.vala b/src/Html.vala index da6a18f..da58fdf 100644 --- a/src/Html.vala +++ b/src/Html.vala @@ -1,30 +1,46 @@ public class Tootle.Html { public static string remove_tags (string content) { - var all_tags = new Regex("<(.|\n)*?>", RegexCompileFlags.CASELESS); - return all_tags.replace(content, -1, 0, ""); + var all_tags = new Regex ("<(.|\n)*?>", RegexCompileFlags.CASELESS); + return GLib.Markup.escape_text (all_tags.replace (content, -1, 0, "")); } - public static string simplify (string content) { - var divided = content + public static string escape_pango_entities (string str) { + return str + .replace (" ", " ") + .replace ("'", "'") + .replace ("& ", "&"); + } + + public static string restore_entities (string str) { + return str + .replace ("&", "&") + .replace ("<", "<") + .replace (">", ">") + .replace ("'", "'") + .replace (""", "\""); + } + + public static string simplify (string str) { + var divided = str .replace("
", "\n") .replace("
", "") .replace("
", "\n") .replace("

", "") .replace("

", "\n\n"); - var html_params = new Regex("(class|target|rel)=\"(.|\n)*?\"", RegexCompileFlags.CASELESS); - var simplified = html_params.replace(divided, -1, 0, ""); + var html_params = new Regex ("(class|target|rel)=\"(.|\n)*?\"", RegexCompileFlags.CASELESS); + var simplified = html_params.replace (divided, -1, 0, ""); while (simplified.has_suffix ("\n")) simplified = simplified.slice (0, simplified.last_index_of ("\n")); - return simplified; + return escape_pango_entities (simplified); } - public static string uri_encode (string content) { - var to_escape = ";&+"; - return Soup.URI.encode (content, to_escape); + public static string uri_encode (string str) { + var restored = restore_entities (str); + return Soup.URI.encode (restored, ";&+"); } } diff --git a/src/ImageCache.vala b/src/ImageCache.vala deleted file mode 100644 index cd625a8..0000000 --- a/src/ImageCache.vala +++ /dev/null @@ -1,148 +0,0 @@ -using Soup; -using GLib; -using Gdk; -using Json; - -private struct CachedImage { - - public string uri; - public int size; - - public CachedImage (string _uri, int _size) { - uri = _uri; - size = _size; - } - - public static uint hash(CachedImage? c) { - assert (c != null); - assert (c.uri != null); - return GLib.int64_hash (c.size) ^ c.uri.hash (); - } - - public static bool equal (CachedImage? a, CachedImage? b) { - if (a == null || b == null) - return false; - return a.size == b.size && a.uri == b.uri; - } - -} - -public delegate void PixbufCallback (Gdk.Pixbuf pb); - -public class Tootle.ImageCache : GLib.Object { - - private GLib.HashTable in_progress; - private GLib.HashTable pixbufs; - private uint total_size_est; - private uint size_limit; - private string cache_path; - - construct { - pixbufs = new GLib.HashTable (CachedImage.hash, CachedImage.equal); - in_progress = new GLib.HashTable (CachedImage.hash, CachedImage.equal); - total_size_est = 0; - cache_path = "%s/%s".printf (GLib.Environment.get_user_cache_dir (), app.application_id); - - settings.changed.connect (on_settings_changed); - on_settings_changed (); - } - - public ImageCache() {} - - private void on_settings_changed () { - // assume 32BPP (divide bytes by 4 to get # pixels) and raw, overhead-free storage - // cache_size setting is number of megabytes - size_limit = (1024 * 1024 * settings.cache_size) / 4; - if (settings.cache) - enforce_size_limit (); - else - remove_all (); - } - - public void remove_all () { - debug("Image cache cleared"); - pixbufs.remove_all (); - total_size_est = 0; - } - - public void remove_one (string uri, int size) { - CachedImage ci = CachedImage (uri, size); - bool removed = pixbufs.remove(ci); - if (removed) { - assert (total_size_est >= size * size); - total_size_est -= size * size; - debug("Cache usage: %zd", total_size_est); - } - } - - //TODO: fix me - // remove least used image - private void remove_least_used () { - var keys = pixbufs.get_keys(); - if (keys.first() != null) { - var ci = keys.first().data; - remove_one(ci.uri, ci.size); - } - } - - private void enforce_size_limit () { - debug("Updating size limit (%zd/%zd)", total_size_est, size_limit); - while (total_size_est > size_limit && pixbufs.size() > 0) - remove_least_used (); - - assert (total_size_est <= size_limit); - } - - private void store_pixbuf (CachedImage ci, Gdk.Pixbuf pixbuf) { - assert (!pixbufs.contains (ci)); - pixbufs.insert (ci, pixbuf); - in_progress.remove (ci); - total_size_est += ci.size * ci.size; - enforce_size_limit (); - } - - public async void get_image (string uri, int size, owned PixbufCallback? cb = null) { - CachedImage ci = CachedImage (uri, size); - Gdk.Pixbuf? pb = pixbufs.get(ci); - if (pb != null) { - cb (pb); - return; - } - - Soup.Message? msg = in_progress.get(ci); - if (msg == null) { - msg = new Soup.Message("GET", uri); - ulong id = 0; - id = msg.finished.connect(() => { - debug("Caching %s@%d", uri, size); - var data = msg.response_body.data; - var stream = new MemoryInputStream.from_data (data); - var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true); - store_pixbuf(ci, pixbuf); - cb(pixbuf); - msg.disconnect(id); - }); - in_progress[ci] = msg; - network.queue (msg); - } else { - ulong id = 0; - id = msg.finished.connect(() => { - cb(pixbufs[ci]); - msg.disconnect(id); - }); - } - } - - public void load_avatar (string uri, Granite.Widgets.Avatar avatar, int size) { - get_image.begin (uri, size, (pixbuf) => avatar.pixbuf = pixbuf.scale_simple (size, size, Gdk.InterpType.BILINEAR)); - } - - public void load_image (string uri, Gtk.Image image) { - load_scaled_image (uri, image, -1); - } - - public void load_scaled_image (string uri, Gtk.Image image, int size) { - get_image.begin (uri, size, image.set_from_pixbuf); - } - -} diff --git a/src/InstanceAccount.vala b/src/InstanceAccount.vala index 431c3ac..6532ed7 100644 --- a/src/InstanceAccount.vala +++ b/src/InstanceAccount.vala @@ -1,58 +1,89 @@ using GLib; using Gee; -public class Tootle.InstanceAccount : Object { +public class Tootle.InstanceAccount : API.Account, IStreamListener { - public string username {get; set;} - public string instance {get; set;} - public string client_id {get; set;} - public string client_secret {get; set;} - public string token {get; set;} + public string instance { get; set; } + public string client_id { get; set; } + public string client_secret { get; set; } + public string token { get; set; } - public int64 last_seen_notification {get; set; default = 0;} - public bool has_unread_notifications {get; set; default = false;} - public ArrayList cached_notifications {get; set;} + public int64 last_seen_notification { get; set; default = 0; } + public bool has_unread_notifications { get; set; default = false; } + public ArrayList cached_notifications { get; set; default = new ArrayList (); } - private Notificator? notificator; + protected string? stream; - public InstanceAccount () { - cached_notifications = new ArrayList (); + public string handle { + owned get { return @"@$username@$short_instance"; } + } + public string short_instance { + owned get { + return instance + .replace ("https://", "") + .replace ("/",""); + } } - public string get_pretty_instance () { - return instance - .replace ("https://", "") - .replace ("/",""); + public InstanceAccount (Json.Object obj) { + Object ( + username: obj.get_string_member ("username"), + instance: obj.get_string_member ("instance"), + client_id: obj.get_string_member ("id"), + client_secret: obj.get_string_member ("secret"), + token: obj.get_string_member ("access_token"), + last_seen_notification: obj.get_int_member ("last_seen_notification"), + has_unread_notifications: obj.get_boolean_member ("has_unread_notifications") + ); + + var cached = obj.get_object_member ("cached_profile"); + var account = new API.Account (cached); + patch (account); + + var notifications = obj.get_array_member ("cached_notifications"); + notifications.foreach_element ((arr, i, node) => { + var notification = new API.Notification (node.get_object ()); + cached_notifications.add (notification); + }); + } + ~InstanceAccount () { + unsubscribe (); + } + + public InstanceAccount.empty (string instance){ + Object (id: 0, instance: instance); } - public void start_notificator () { - if (notificator != null) - notificator.close (); - - notificator = new Notificator (get_stream ()); - notificator.status_added.connect (status_added); - notificator.status_removed.connect (status_removed); - notificator.notification.connect (notification); - notificator.start (); + public InstanceAccount.from_account (API.Account account) { + Object (id: account.id); + patch (account); } + public InstanceAccount patch (API.Account account) { + Utils.merge (this, account); + return this; + } + public bool is_current () { - return accounts.formal.token == token; + return accounts.active.token == token; } - public Soup.Message get_stream () { - var url = "%s/api/v1/streaming/?stream=user&access_token=%s".printf (instance, token); - return new Soup.Message ("GET", url); + public string get_stream_url () { + return @"$instance/api/v1/streaming/?stream=user&access_token=$token"; } - public void close_notificator () { - if (notificator != null) - notificator.close (); + public void subscribe () { + streams.subscribe (get_stream_url (), this, out stream); } - public Json.Node serialize () { + public void unsubscribe () { + streams.unsubscribe (stream, this); + } + + public override Json.Node? serialize () { var builder = new Json.Builder (); builder.begin_object (); + builder.set_member_name ("hash"); builder.add_string_value ("test"); builder.set_member_name ("username"); @@ -63,13 +94,17 @@ public class Tootle.InstanceAccount : Object { builder.add_string_value (client_id); builder.set_member_name ("secret"); builder.add_string_value (client_secret); - builder.set_member_name ("token"); + builder.set_member_name ("access_token"); builder.add_string_value (token); builder.set_member_name ("last_seen_notification"); builder.add_int_value (last_seen_notification); builder.set_member_name ("has_unread_notifications"); builder.add_boolean_value (has_unread_notifications); + var cached_profile = base.serialize (); + builder.set_member_name ("cached_profile"); + builder.add_value (cached_profile); + builder.set_member_name ("cached_notifications"); builder.begin_array (); cached_notifications.@foreach (notification => { @@ -84,31 +119,12 @@ public class Tootle.InstanceAccount : Object { return builder.get_root (); } - public static InstanceAccount parse (Json.Object obj) { - var acc = new InstanceAccount (); - acc.username = obj.get_string_member ("username"); - acc.instance = obj.get_string_member ("instance"); - acc.client_id = obj.get_string_member ("id"); - acc.client_secret = obj.get_string_member ("secret"); - acc.token = obj.get_string_member ("token"); - acc.last_seen_notification = obj.get_int_member ("last_seen_notification"); - acc.has_unread_notifications = obj.get_boolean_member ("has_unread_notifications"); - - var notifications = obj.get_array_member ("cached_notifications"); - notifications.foreach_element ((arr, i, node) => { - var notification = API.Notification.parse (node.get_object ()); - acc.cached_notifications.add (notification); - }); - - return acc; - } - - public void notification (API.Notification obj) { - var title = Html.remove_tags (obj.type.get_desc (obj.account)); + public override void on_notification (API.Notification obj) { + var title = Html.remove_tags (obj.kind.get_desc (obj.account)); var notification = new GLib.Notification (title); if (obj.status != null) { var body = ""; - body += get_pretty_instance (); + body += short_instance; body += "\n"; body += Html.remove_tags (obj.status.content); notification.set_body (body); @@ -118,34 +134,34 @@ public class Tootle.InstanceAccount : Object { app.send_notification (app.application_id + ":" + obj.id.to_string (), notification); if (is_current ()) - network.notification (obj); + streams.notification (obj); - if (obj.type == API.NotificationType.WATCHLIST) { + if (obj.kind == API.NotificationType.WATCHLIST) { cached_notifications.add (obj); accounts.save (); } } - private void status_removed (int64 id) { + public override void on_status_removed (int64 id) { if (is_current ()) - network.status_removed (id); + streams.status_removed (id); } - private void status_added (API.Status status) { + public override void on_status_added (API.Status status) { if (!is_current ()) return; - watchlist.users.@foreach (item => { - var acct = status.account.acct; - if (item == acct || item == "@" + acct) { - var obj = new API.Notification (-1); - obj.type = API.NotificationType.WATCHLIST; - obj.account = status.account; - obj.status = status; - notification (obj); - } - return true; - }); + // watchlist.users.@foreach (item => { + // var acct = status.account.acct; + // if (item == acct || item == "@" + acct) { + // var obj = new API.Notification (-1); + // obj.kind = API.NotificationType.WATCHLIST; + // obj.account = status.account; + // obj.status = status; + // on_notification (obj); + // } + // return true; + // }); } } diff --git a/src/Network.vala b/src/Network.vala deleted file mode 100644 index 155af42..0000000 --- a/src/Network.vala +++ /dev/null @@ -1,208 +0,0 @@ -using Soup; -using GLib; -using Gdk; -using Json; - -public class Tootle.Network : GLib.Object { - - public const string INJECT_TOKEN = "X-HeyMate-PlsInjectToken4MeThx"; - - public signal void started (); - public signal void finished (); - public signal void notification (API.Notification notification); - public signal void status_removed (int64 id); - - public delegate void ErrorCallback (int32 code, string reason); - public delegate void SuccessCallback (Session session, Message msg) throws GLib.Error; - - private int requests_processing = 0; - private Soup.Session session; - - construct { - session = new Soup.Session (); - session.ssl_strict = true; - session.ssl_use_system_ca_file = true; - session.timeout = 15; - session.max_conns = 20; - session.request_unqueued.connect (msg => { - requests_processing--; - if (requests_processing <= 0) - finished (); - }); - - // Soup.Logger logger = new Soup.Logger (Soup.LoggerLogLevel.BODY, -1); - // session.add_feature (logger); - } - - public Network () {} - - public async WebsocketConnection stream (Soup.Message msg) throws GLib.Error { - return yield session.websocket_connect_async (msg, null, null, null); - } - - public void cancel_request (Soup.Message? msg) { - if (msg == null) - return; - switch (msg.status_code) { - case Soup.Status.CANCELLED: - case Soup.Status.OK: - return; - } - session.cancel_message (msg, Soup.Status.CANCELLED); - } - - public void inject (Soup.Message msg, string header) { - msg.request_headers.append (header, "VeryPls"); - } - - private void inject_headers (ref Soup.Message msg) { - var headers = msg.request_headers; - var formal = accounts.formal; - if (headers.get_one (INJECT_TOKEN) != null && formal != null) { - headers.remove (INJECT_TOKEN); - headers.append ("Authorization", "Bearer " + formal.token); - } - } - - public void queue (owned Soup.Message message, owned SuccessCallback? cb = null, owned ErrorCallback? errcb = null) { - requests_processing++; - started (); - - inject_headers (ref message); - - session.queue_message (message, (sess, msg) => { - var status = msg.status_code; - if (status != Soup.Status.CANCELLED) { - if (status == Soup.Status.OK) { - if (cb != null) { - try { - cb (session, msg); - } - catch (Error e) { - warning ("Caught exception on network request:"); - warning (e.message); - if (errcb != null) - errcb (Soup.Status.NONE, e.message); - } - } - } - else { - if (errcb != null) - errcb ((int32)status, get_error_reason ((int32)status)); - } - } - // msg.request_body.free (); - // msg.response_body.free (); - // msg.request_headers.free (); - // msg.response_headers.free (); - }); - } - - public string get_error_reason (int32 status) { - return "Error " + status.to_string () + ": " + Soup.Status.get_phrase (status); - } - - public void on_error (int32 code, string message) { - warning (message); - app.toast (message); - } - - public void on_show_error (int32 code, string message) { - warning (message); - app.error (_("Network Error"), message); - } - - public Json.Object parse (Soup.Message msg) throws GLib.Error { - // debug ("Status Code: %u", msg.status_code); - // debug ("Message length: %lld", msg.response_body.length); - // debug ("Object: %s", (string) msg.response_body.data); - - var parser = new Json.Parser (); - parser.load_from_data ((string) msg.response_body.flatten ().data, -1); - return parser.get_root ().get_object (); - } - - public Json.Array parse_array (Soup.Message msg) throws GLib.Error { - // debug ("Status Code: %u", msg.status_code); - // debug ("Message length: %lld", msg.response_body.length); - // debug ("Array: %s", (string) msg.response_body.data); - - var parser = new Json.Parser (); - parser.load_from_data ((string) msg.response_body.flatten ().data, -1); - return parser.get_root ().get_array (); - } - - //TODO: Cache - public void load_avatar (string url, Granite.Widgets.Avatar avatar, int size){ - var message = new Soup.Message("GET", url); - network.queue (message, (sess, msg) => { - if (msg.status_code != Soup.Status.OK) { - avatar.show_default (size); - return; - } - - var data = msg.response_body.data; - var stream = new MemoryInputStream.from_data (data); - var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true); - - avatar.pixbuf = pixbuf.scale_simple (size, size, Gdk.InterpType.BILINEAR); - }); - } - - //TODO: Cache - public delegate void PixbufCallback (Gdk.Pixbuf pixbuf); - public Soup.Message load_pixbuf (string url, PixbufCallback cb) { - var message = new Soup.Message("GET", url); - network.queue (message, (sess, msg) => { - Gdk.Pixbuf? pixbuf = null; - try { - var data = msg.response_body.flatten ().data; - var stream = new MemoryInputStream.from_data (data); - pixbuf = new Gdk.Pixbuf.from_stream (stream); - } - catch (Error e) { - warning ("Can't get image: %s".printf (url)); - warning ("Reason: " + e.message); - } - finally { - if (msg.status_code != Soup.Status.OK) - warning ("Invalid response code %s: %s".printf (msg.status_code.to_string (), url)); - } - cb (pixbuf); - }); - return message; - } - - //TODO: Cache - public void load_image (string url, Gtk.Image image) { - var message = new Soup.Message("GET", url); - network.queue (message, (sess, msg) => { - if (msg.status_code != Soup.Status.OK) { - image.set_from_icon_name ("image-missing", Gtk.IconSize.LARGE_TOOLBAR); - return; - } - - var data = msg.response_body.data; - var stream = new MemoryInputStream.from_data (data); - var pixbuf = new Gdk.Pixbuf.from_stream (stream); - image.set_from_pixbuf (pixbuf); - }); - } - - //TODO: Cache - public void load_scaled_image (string url, Gtk.Image image, int size) { - var message = new Soup.Message("GET", url); - network.queue (message, (sess, msg) => { - if (msg.status_code != Soup.Status.OK) { - image.set_from_icon_name ("image-missing", Gtk.IconSize.LARGE_TOOLBAR); - return; - } - - var data = msg.response_body.data; - var stream = new MemoryInputStream.from_data (data); - var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true); - image.set_from_pixbuf (pixbuf); - }); - } - -} diff --git a/src/Notificator.vala b/src/Notificator.vala deleted file mode 100644 index 1585a1a..0000000 --- a/src/Notificator.vala +++ /dev/null @@ -1,122 +0,0 @@ -using GLib; -using Soup; - -public class Tootle.Notificator : GLib.Object { - - private WebsocketConnection? connection; - private Soup.Message msg; - private bool closing = false; - private int timeout = 2; - - public signal void notification (API.Notification notification); - public signal void status_added (API.Status status); - public signal void status_removed (int64 id); - - public Notificator (Soup.Message _msg){ - msg = _msg; - msg.priority = Soup.MessagePriority.VERY_HIGH; - msg.set_flags (Soup.MessageFlags.IGNORE_CONNECTION_LIMITS); - } - - public string get_url () { - return msg.get_uri ().to_string (false); - } - - public string get_name () { - var name = msg.get_uri ().to_string (true); - if ("&access_token" in name) { - var pos = name.last_index_of ("&access_token"); - name = name.slice (0, pos); - } - - return name; - } - - public async void start () { - if (connection != null) - return; - - try { - info ("Starting: %s", get_name ()); - connection = yield network.stream (msg); - connection.error.connect (on_error); - connection.message.connect (on_message); - connection.closed.connect (on_closed); - timeout = 2; - } - catch (GLib.Error e) { - warning (e.message); - on_closed (); - } - } - - public void close () { - if (connection == null) - return; - - info ("Closing: %s", get_name ()); - closing = true; - connection.close (0, null); - } - - private bool reconnect () { - start (); - return false; - } - - private void on_closed () { - if (closing) - return; - - warning ("Aborted: %s. Reconnecting in %i seconds.", get_name (), timeout); - GLib.Timeout.add_seconds (timeout, reconnect); - timeout = int.min (timeout*2, 60); - } - - private void on_error (Error e) { - if (!closing) - warning ("Error in %s: %s", get_name (), e.message); - } - - private void on_message (int i, Bytes bytes) { - var msg = (string) bytes.get_data (); - - var parser = new Json.Parser (); - parser.load_from_data (msg, -1); - var root = parser.get_root ().get_object (); - - var type = root.get_string_member ("event"); - switch (type) { - case "update": - if (!settings.live_updates) - return; - - var status = API.Status.parse (sanitize (root)); - status_added (status); - break; - case "delete": - if (!settings.live_updates) - return; - - var id = int64.parse (root.get_string_member("payload")); - status_removed (id); - break; - case "notification": - var notif = API.Notification.parse (sanitize (root)); - notification (notif); - break; - default: - warning ("Unknown push event: %s", type); - break; - } - } - - private Json.Object sanitize (Json.Object root) { - var payload = root.get_string_member ("payload"); - var sanitized = Soup.URI.decode (payload); - var parser = new Json.Parser (); - parser.load_from_data (sanitized, -1); - return parser.get_root ().get_object (); - } - -} diff --git a/src/Request.vala b/src/Request.vala new file mode 100644 index 0000000..1d0276b --- /dev/null +++ b/src/Request.vala @@ -0,0 +1,110 @@ +using Soup; +using Gee; + +public class Tootle.Request : Soup.Message { + + public string url { construct set; get; } + private Network.SuccessCallback? cb; + private Network.ErrorCallback? error_cb; + private HashMap? pars; + private weak InstanceAccount? account; + private bool needs_token = false; + + public Request.GET (string url) { + Object (method: "GET", url: url); + } + public Request.POST (string url) { + Object (method: "POST", url: url); + } + public Request.DELETE (string url) { + Object (method: "DELETE", url: url); + } + + public Request then (owned Network.SuccessCallback cb) { + this.cb = (owned) cb; + return this; + } + + public Request then_parse_array (owned Network.NodeCallback _cb) { + this.cb = (sess, msg) => { + var parser = new Json.Parser (); + parser.load_from_data ((string) msg.response_body.flatten ().data, -1); + parser.get_root ().get_array ().foreach_element ((array, i, node) => _cb (node, msg)); + }; + return this; + } + + public Request then_parse_obj (owned Network.ObjectCallback _cb) { + this.cb = (sess, msg) => { + _cb (network.parse (msg)); + }; + return this; + } + + public Request on_error (owned Network.ErrorCallback cb) { + this.error_cb = (owned) cb; + return this; + } + + public Request with_account (InstanceAccount? account = null) { + this.needs_token = true; + if (account != null) + this.account = account; + return this; + } + + public Request with_param (string name, string val) { + if (pars == null) + pars = new HashMap (); + pars[name] = val; + return this; + } + + // Should be used for requests with default priority + public Request queue () { + var parameters = ""; + if (pars != null) { + parameters = "?"; + var parameters_counter = 0; + pars.@foreach (entry => { + parameters_counter++; + var key = (string) entry.key; + var val = (string) entry.value; + parameters += @"$key=$val"; + + if (parameters_counter < pars.size) + parameters += "&"; + + return true; + }); + } + + if (needs_token) { + if (account == null) { + warning (@"No account found for: $method: $url$parameters"); + return this; + } + + request_headers.append ("Authorization", @"Bearer $(account.token)"); + } + + if (!("://" in url)) { + url = account.instance + url; + } + + this.uri = new URI (url + "" + parameters); + + url = uri.to_string (false); + info (@"$method: $url"); + + network.queue (this, (owned) cb, (owned) error_cb); + return this; + } + + // Should be used for real-time user interactions (liking, removing and browsing posts) + public Request exec () { + this.priority = MessagePriority.HIGH; + return this.queue (); + } + +} diff --git a/src/Services/Accounts.vala b/src/Services/Accounts.vala new file mode 100644 index 0000000..99c217b --- /dev/null +++ b/src/Services/Accounts.vala @@ -0,0 +1,141 @@ +using Gee; + +public class Tootle.Accounts : GLib.Object { + + private string dir_path; + private string file_path; + + public ArrayList saved { get; set; default = new ArrayList (); } + public InstanceAccount? active { get; set; } + + construct { + dir_path = @"$(GLib.Environment.get_user_config_dir ())/$(app.application_id)"; + file_path = @"$dir_path/accounts.json"; + } + + public void switch_account (int id) { + var acc = saved.@get (id); + info (@"Switching to account: $(acc.handle)..."); + new Request.GET ("/api/v1/accounts/verify_credentials") + .with_account (acc) + .then ((sess, mess) => { + var root = network.parse (mess); + var profile = new API.Account (root); + acc.patch (profile); + info ("OK: Token is valid"); + active = acc; + settings.current_account = id; + }) + .on_error ((code, reason) => { + warning ("Token invalid!"); + network.on_show_error (code, _("This instance has invalidated this session. Please sign in again.\n\n%s").printf (reason)); + }) + .exec (); + } + + public void add (InstanceAccount account) { + info (@"Adding new account: $(account.handle)"); + saved.add (account); + save (); + switch_account (saved.size - 1); + account.subscribe (); + } + + public void remove (InstanceAccount account) { + account.unsubscribe (); + saved.remove (account); + saved.notify_property ("size"); + + if (saved.size < 1) + active = null; + else { + var id = settings.current_account - 1; + if (id > saved.size - 1) + id = saved.size - 1; + else if (id < saved.size - 1) + id = 0; + switch_account (id); + } + save (); + + if (is_empty ()) + window.open_view (new Views.NewAccount (false)); + } + + public bool is_empty () { + return saved.size == 0; + } + + public void init () { + save (false); + load (); + + if (saved.size < 1) + window.open_view (new Views.NewAccount (false)); + else + switch_account (settings.current_account); + } + + public void save (bool overwrite = true) { + try { + var dir = File.new_for_path (dir_path); + if (!dir.query_exists ()) + dir.make_directory (); + + var file = File.new_for_path (file_path); + if (file.query_exists () && !overwrite) + return; + + var builder = new Json.Builder (); + builder.begin_array (); + saved.foreach ((acc) => { + var node = acc.serialize (); + builder.add_value (node); + return true; + }); + builder.end_array (); + + var generator = new Json.Generator (); + generator.set_root (builder.get_root ()); + var data = generator.to_data (null); + + if (file.query_exists ()) + file.@delete (); + + FileOutputStream stream = file.create (FileCreateFlags.PRIVATE); + stream.write (data.data); + info ("Saved accounts"); + } + catch (Error e){ + warning (e.message); + } + } + + private void load () { + try { + uint8[] data; + string etag; + var file = File.new_for_path (file_path); + file.load_contents (null, out data, out etag); + var contents = (string) data; + + var parser = new Json.Parser (); + parser.load_from_data (contents, -1); + var array = parser.get_root ().get_array (); + + array.foreach_element ((_arr, _i, node) => { + var obj = node.get_object (); + var account = new InstanceAccount (obj); + if (account != null) { + saved.add (account); + account.subscribe (); + } + }); + info (@"Loaded $(saved.size) accounts"); + } + catch (Error e){ + warning (e.message); + } + } + +} diff --git a/src/Services/Cache.vala b/src/Services/Cache.vala new file mode 100644 index 0000000..3722281 --- /dev/null +++ b/src/Services/Cache.vala @@ -0,0 +1,136 @@ +using Gee; +using Gdk; + +public class Tootle.Cache : GLib.Object { + + protected HashTable items { get; set; } + protected HashTable items_in_progress { get; set; } + protected uint size { + get { + return items.size (); + } + } + + construct { + items = new HashTable (GLib.str_hash, GLib.str_equal); + items_in_progress = new HashTable (GLib.str_hash, GLib.str_equal); + } + + public delegate void CachedResultCallback (Reference? result); + + public struct Reference { + public string key; + public weak Pixbuf? data; + public bool loading; + } + + protected class Item : GLib.Object { + public Pixbuf data { get; construct set; } + public int64 references { get; construct set; } + + public Item (Pixbuf d, int64 r) { + Object (data: d, references: r); + } + } + + public void unload (Reference? r) { + if (r == null) + return; + + if (r.data == null) + return; + + var item = items[r.key]; + if (item == null) + return; + + item.references--; + //info (@"DEREF $(r.key) $(item.references)"); + if (item.references <= 0) { + //info ("REMOVE %s", r.key); + items.remove (r.key); + items_in_progress.remove (r.key); + } + } + + public void load (string? url, owned CachedResultCallback cb) { + if (url == null) + return; + + var key = url; + if (items.contains (key)) { + //info (@"LOAD $key"); + var item = items.@get (key); + item.references++; + cb (Reference () { + data = item.data, + key = key, + loading = false + }); + return; + } + + var item = items.@get (key); + + var message = items_in_progress.@get (key); + if (message == null) { + message = new Soup.Message ("GET", url); + ulong id = 0; + id = message.finished.connect (() => { + Pixbuf? pixbuf = null; + + var data = message.response_body.flatten ().data; + var stream = new MemoryInputStream.from_data (data); + pixbuf = new Pixbuf.from_stream (stream); + stream.close (); + + //info (@"< STORE $key"); + items[key] = new Item (pixbuf, 1); + items_in_progress.remove (key); + + cb (Reference () { + data = items[key].data, + key = key, + loading = false + }); + + message.disconnect (id); + }); + + network.queue (message, (sess, msg) => { + // no one cares + }, + (code, reason) => { + cb (null); + }); + + cb (Reference () { + data = null, + key = key, + loading = true + }); + + items_in_progress.insert (key, message); + } + else { + //info ("AWAIT: %s", key); + ulong id = 0; + id = message.finished.connect_after (() => { + var it = items.@get (key); + cb (Reference () { + data = it.data, + key = key, + loading = false + }); + it.references++; + message.disconnect (id); + }); + } + } + + public void clear () { + info ("PURGE"); + items.remove_all (); + items_in_progress.remove_all (); + } +} diff --git a/src/Services/IAccountListener.vala b/src/Services/IAccountListener.vala new file mode 100644 index 0000000..75e1d8e --- /dev/null +++ b/src/Services/IAccountListener.vala @@ -0,0 +1,12 @@ +public interface Tootle.IAccountListener : GLib.Object { + + protected void connect_account () { + accounts.notify["active"].connect (() => on_account_changed (accounts.active)); + accounts.saved.notify["size"].connect (() => on_accounts_changed (accounts.saved)); + on_account_changed (accounts.active); + } + + public virtual void on_account_changed (InstanceAccount? account) {} + public virtual void on_accounts_changed (Gee.ArrayList accounts) {} + +} diff --git a/src/Services/IStreamListener.vala b/src/Services/IStreamListener.vala new file mode 100644 index 0000000..2717322 --- /dev/null +++ b/src/Services/IStreamListener.vala @@ -0,0 +1,11 @@ +public interface Tootle.IStreamListener : GLib.Object { + + public virtual void on_status_removed (int64 id) {} + public virtual void on_status_added (API.Status s) {} + public virtual void on_notification (API.Notification n) {} + + public virtual bool accepts (ref string event) { + return true; + } + +} diff --git a/src/Services/Network.vala b/src/Services/Network.vala new file mode 100644 index 0000000..2d3ed83 --- /dev/null +++ b/src/Services/Network.vala @@ -0,0 +1,88 @@ +using Soup; +using GLib; +using Gdk; +using Json; + +public class Tootle.Network : GLib.Object { + + public signal void started (); + public signal void finished (); + + public delegate void ErrorCallback (int32 code, string reason); + public delegate void SuccessCallback (Session session, Message msg) throws Error; + public delegate void NodeCallback (Json.Node node, Message msg) throws Error; + public delegate void ObjectCallback (Json.Object node) throws Error; + + private int requests_processing = 0; + public Soup.Session session; + + construct { + session = new Soup.Session (); + session.ssl_strict = true; + session.ssl_use_system_ca_file = true; + session.timeout = 15; + session.max_conns = 30; + session.request_unqueued.connect (msg => { + requests_processing--; + if (requests_processing <= 0) + finished (); + }); + } + + // public void cancel_request (Soup.Message? msg) { + // if (msg == null) + // return; + + // switch (msg.status_code) { + // case Soup.Status.CANCELLED: + // case Soup.Status.OK: + // return; + // } + // session.cancel_message (msg, Soup.Status.CANCELLED); + // } + + public void queue (owned Soup.Message message, owned SuccessCallback? cb, owned ErrorCallback? errcb = null) { + requests_processing++; + started (); + + session.queue_message (message, (sess, msg) => { + var status = msg.status_code; + if (status == Soup.Status.OK) { + try { + cb (session, msg); + } + catch (Error e) { + warning ("Exception on network request: %s", e.message); + if (errcb != null) + errcb (Soup.Status.NONE, e.message); + } + } + else { + if (errcb != null) + errcb ((int32)status, describe_error ((int32)status)); + } + }); + } + + public string describe_error (int32 code) { + var reason = Soup.Status.get_phrase (code); + return @"$code: $reason"; + } + + public void on_error (int32 code, string message) { + warning (message); + app.toast (message); + } + + public void on_show_error (int32 code, string message) { + warning (message); + app.error (_("Network Error"), message); + } + + public Json.Object parse (Soup.Message msg) throws Error { + var parser = new Json.Parser (); + parser.load_from_data ((string) msg.response_body.flatten ().data, -1); + return parser.get_root ().get_object (); + } + +} diff --git a/src/Services/Settings.vala b/src/Services/Settings.vala new file mode 100644 index 0000000..73ec381 --- /dev/null +++ b/src/Services/Settings.vala @@ -0,0 +1,44 @@ +using GLib; + +public class Tootle.Settings : GLib.Settings { + + public int current_account { get; set; } + public bool notifications { get; set; } + public bool always_online { get; set; } + public int char_limit { get; set; } + public bool live_updates { get; set; } + public bool live_updates_public { get; set; } + public bool dark_theme { get; set; } + + public string watched_users { get; set; } + public string watched_hashtags { get; set; } + + public int window_x { get; set; } + public int window_y { get; set; } + public int window_w { get; set; } + public int window_h { get; set; } + + public Settings () { + Object (schema_id: Build.DOMAIN); + init ("current-account"); + init ("notifications"); + init ("always-online"); + init ("char-limit"); + init ("live-updates"); + init ("live-updates-public"); + init ("dark-theme"); + + init ("watched-users"); + init ("watched-hashtags"); + + init ("window-x"); + init ("window-y"); + init ("window-w"); + init ("window-h"); + } + + void init (string key) { + bind (key, this, key, SettingsBindFlags.DEFAULT); + } + +} diff --git a/src/Services/Streams.vala b/src/Services/Streams.vala new file mode 100644 index 0000000..6f10208 --- /dev/null +++ b/src/Services/Streams.vala @@ -0,0 +1,177 @@ +using GLib; +using Soup; +using Gee; + +public class Tootle.Streams : Object { + + public signal void notification (API.Notification n); + public signal void status_removed (int64 id); + + protected HashTable connections { + get; + set; + default = new HashTable (GLib.str_hash, GLib.str_equal); + } + + protected class Connection : Object { + public ArrayList subscribers; + protected WebsocketConnection socket; + protected Message msg; + + protected bool closing = false; + protected int timeout = 2; + + public string name { + owned get { + var url = msg.get_uri ().to_string (false); + return url.slice (0, url.last_index_of ("&access_token")); + } + } + + public Connection (string url) { + this.subscribers = new ArrayList (); + this.msg = new Message ("GET", url); + } + + public bool start () { + //info (@"Opening stream: $name"); + network.session.websocket_connect_async.begin (msg, null, null, null, (obj, res) => { + socket = network.session.websocket_connect_async.end (res); + socket.error.connect (on_error); + socket.closed.connect (on_closed); + socket.message.connect (on_message); + }); + return false; + } + + public void add (IStreamListener s) { + info ("%s > %s", get_subscriber_name (s), name); + subscribers.add (s); + } + + public void remove (IStreamListener s) { + if (subscribers.contains (s)) { + info ("%s X %s", get_subscriber_name (s), name); + subscribers.remove (s); + } + + if (subscribers.size <= 0) { + info (@"Closing: $name"); + closing = true; + socket.close (0, null); + } + } + + void on_error (Error e) { + if (!closing) + warning (@"Error in $name: $(e.message)"); + } + + void on_closed () { + if (!closing) { + warning (@"CLOSED: $name. Reconnecting in $timeout seconds."); + GLib.Timeout.add_seconds (timeout, start); + timeout = int.min (timeout*2, 30); + } + } + + void on_message (int i, Bytes bytes) { + try { + emit (bytes, this); + } + catch (Error e) { + warning (@"Couldn't handle websocket event. Reason: $(e.message)"); + } + } + } + + public void subscribe (string? url, IStreamListener s, out string cookie) { + if (url == null) + return; + + if (connections.contains (url)) { + connections[url].add (s); + } + else { + var con = new Connection (url); + connections[url] = con; + con.add (s); + con.start (); + } + cookie = url; + } + + public void unsubscribe (string? cookie, IStreamListener s) { + var url = cookie; + if (url == null) + return; + + if (connections.contains (url)) + connections.@get (url).remove (s); + } + + static string get_subscriber_name (Object s) { + return s.get_type ().name (); + } + + static void decode (Bytes bytes, out string event, out Json.Object root) throws Error { + var msg = (string) bytes.get_data (); + var parser = new Json.Parser (); + parser.load_from_data (msg, -1); + root = parser.get_root ().get_object (); + event = root.get_string_member ("event"); + } + + static Json.Object sanitize (Json.Object root) { + var payload = root.get_string_member ("payload"); + var sanitized = Soup.URI.decode (payload); + var parser = new Json.Parser (); + parser.load_from_data (sanitized, -1); + return parser.get_root ().get_object (); + } + + static void emit (Bytes bytes, Connection c) throws Error { + if (!settings.live_updates) + return; + + string event; + Json.Object root; + decode (bytes, out event, out root); + + // c.subscribers.@foreach (s => { + // warning ("%s: %s for %s", c.name, event, get_subscriber_name (s)); + // return false; + // }); + + switch (event) { + case "update": + var entity = new API.Status (sanitize (root)); + c.subscribers.@foreach (s => { + if (s.accepts (ref event)) + s.on_status_added (entity); + return false; + }); + break; + case "delete": + var id = int64.parse (root.get_string_member ("payload")); + c.subscribers.@foreach (s => { + if (s.accepts (ref event)) + s.on_status_removed (id); + return false; + }); + break; + case "notification": + var entity = new API.Notification (sanitize (root)); + c.subscribers.@foreach (s => { + if (s.accepts (ref event)) + s.on_notification (entity); + return false; + }); + break; + default: + warning (@"Unknown websocket event: \"$event\". Ignoring."); + break; + } + } + +} diff --git a/src/Settings.vala b/src/Settings.vala deleted file mode 100644 index 2ac0bb7..0000000 --- a/src/Settings.vala +++ /dev/null @@ -1,24 +0,0 @@ -public class Tootle.Settings : Granite.Services.Settings { - - public int current_account { get; set; } - public bool notifications { get; set; } - public bool always_online { get; set; } - public bool cache { get; set; } - public int cache_size { get; set; } - public int char_limit { get; set; } - public bool live_updates { get; set; } - public bool live_updates_public { get; set; } - public bool dark_theme { get; set; } - public string watched_users { get; set; } - public string watched_hashtags { get; set; } - - public int window_x { get; set; } - public int window_y { get; set; } - public int window_w { get; set; } - public int window_h { get; set; } - - public Settings () { - base ("com.github.bleakgrey.tootle"); - } - -} diff --git a/src/Stacktrace.vala b/src/Stacktrace.vala new file mode 100644 index 0000000..adf65b1 --- /dev/null +++ b/src/Stacktrace.vala @@ -0,0 +1,651 @@ +/* + * Copyright (C) 2014 PerfectCarl - https://github.com/PerfectCarl/vala-stacktrace + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +public class Stacktrace { + + public class Frame { + // Address used by addr2line + public string address { get;private set;default = "";} + + public string line { get;private set;default = "";} + + public string line_number { get;private set;default = "";} + + public string file_path { get;private set;default = "";} + + public string file_short_path { get;private set;default = "";} + + public string function { get;private set;default = "";} + + public Frame (string address, string line, string function, string file_path, string file_short_path) { + this._address = address; + this._line = line; + + this._file_path = file_path; + this._file_short_path = file_short_path; + this._function = function; + this.line_number = extract_line (line); + } + + public string to_string () { + var result = line; + if (result == "") + result = " C library at address [" + address + "]"; + return result + " [" + address + "]"; + } + + } + + public enum Style { + RESET = 0, + BRIGHT = 1, + DIM = 2, + UNDERLINE = 3, + BLINK = 4, + REVERSE = 7, + HIDDEN = 8 + } + + public enum CriticalHandler { + IGNORE, + PRINT_STACKTRACE, + CRASH + } + + public enum Color { + BLACK = 0, + RED = 1, + GREEN = 2, + YELLOW = 3, + BLUE = 4, + MAGENTA = 5, + CYAN = 6, + WHITE = 7 + } + + public Gee.ArrayList _frames = new Gee.ArrayList(); + + private Frame first_vala = null; + + private int max_file_name_length = 0; + + private int max_line_number_length = 0; + + private bool is_all_function_name_blank = true; + + private bool is_all_file_name_blank = true; + + private ProcessSignal sig; + + public static bool enabled { get;set;default = true;} + + public static bool hide_installed_libraries { get;set;default = false;} + + public static Color default_highlight_color { get;set;default = Color.WHITE;} + + public static Color default_error_background { get;set;default = Color.RED;} + + public Color highlight_color { get;set;default = Color.WHITE;} + + public Color error_background { get;set;default = Color.RED;} + + public Gee.ArrayList frames { + get { + return _frames; + } + } + + public Stacktrace (GLib.ProcessSignal sig = GLib.ProcessSignal.TTOU) { + this.sig = sig; + error_background = default_error_background; + highlight_color = default_highlight_color; + //hide_installed_libraries = true; + create_stacktrace (); + } + + private string get_module_name () { + var path = new char[1024]; + Posix.readlink ("/proc/self/exe", path); + string result = (string) path; + return result; + } + + // TODO CARL convert this piece of code to vala conventions + public static string get_relative_path (string p_fullDestinationPath, string p_startPath) { + + string[] l_startPathParts = p_startPath.split ("/"); + string[] l_destinationPathParts = p_fullDestinationPath.split ("/"); + + int l_sameCounter = 0; + while ((l_sameCounter < l_startPathParts.length) && + (l_sameCounter < l_destinationPathParts.length) && + l_startPathParts[l_sameCounter] == l_destinationPathParts[l_sameCounter]) { + l_sameCounter++; + } + + if (l_sameCounter == 0) { + return p_fullDestinationPath; // There is no relative link. + } + + StringBuilder l_builder = new StringBuilder (); + for (int i = l_sameCounter ; i < l_startPathParts.length ; i++) { + l_builder.append ("../"); + } + + for (int i = l_sameCounter ; i < l_destinationPathParts.length ; i++) { + l_builder.append (l_destinationPathParts[i] + "/"); + } + + // CARL l_builder.Length--; + // Remove the last / + var result = l_builder.str; + result = result.substring (0, result.length - 1); + return result; + } + + private string extract_short_file_path (string file_path) { + var path = Environment.get_current_dir (); + /*var i = file_path.index_of ( path ); + if( i>=0 ) + return file_path.substring ( path.length, file_path.length - path.length ); + return file_path; */ + var result = get_relative_path (file_path, path); + return result; + } + + public bool is_custom { + get { + return sig == ProcessSignal.TTOU; + } + } + // input : '/home/cran/Documents/Projects/elementary/noise/instant-beta/build/core/libnoise-core.so.0(noise_job_repository_create_job+0x309) [0x7ff60a021e69]' + // ouput: 0x309 + private int extract_base_address (string line) { + int result = 0 ; + var start = line.last_index_of ("+"); + if (start >= 0) { + var end = line.last_index_of (")"); + if( end > start ) { + var text = line.substring (start+3,end-start-3) ; + text.scanf("%x", &result); + } + } + return result ; + } + + private void process_info_for_file (string full_line, string str ) { + func = "" ; + file_path = ""; + short_file_path = ""; + l = ""; + file_line = ""; + func_line = ""; + + var lines = full_line.split ("\n"); + + if (lines.length > 0) + func_line = lines[0]; + + if (lines.length > 1) + file_line = lines[1]; + if (file_line == "??:0" || file_line == "??:?") + file_line = ""; + func = extract_function_name (str); + + file_path = ""; + short_file_path = ""; + l = ""; + if (file_line != "") { + if (func == "") + func = extract_function_name_from_line (func_line); + file_path = extract_file_path (file_line); + short_file_path = extract_short_file_path (file_path); + l = extract_line (file_line); + } + } + + string func = "" ; + string file_path = ""; + string short_file_path = ""; + string l = ""; + string file_line = ""; + string func_line = ""; + + private void process_info_from_lib (string file_path, string str) { + var cmd2 = "nm %s".printf(file_path) ; + var addr1_s = execute_command_sync_get_output (cmd2) ; + MatchInfo info ; + try { + Regex regex = new Regex ("\\n[^ ]* T "+func); + + if( regex.match (addr1_s, 0, out info) ) + { + while( info.matches() ){ + var lll = info.fetch(0) ; + //stdout.printf ( "lll '%s'\n", lll ) ; + addr1_s = lll.substring(0, lll.index_of(" ")) ; + info.next(); + } + } + } catch (RegexError e) + { + critical( "Error while processing regex %s", e.message ) ; + } + //stdout.printf ("addr1_s %s\n", addr1_s) ; + int addr1 = 0 ; + addr1_s.scanf("%x", &addr1); + if( addr1 != 0 ) { + int addr2 = extract_base_address (str) ; + string addr3 = "%#08x".printf (addr1+addr2); + var new_full_line = process_line (file_path, addr3); + process_info_for_file (new_full_line, str ) ; + } + } + + private void create_stacktrace () { + int frame_count = 100; + int skipped_frames_count = 5; + // Stacktrace not due to a crash + if (is_custom) + skipped_frames_count = 3; + + void *[] array = new void *[frame_count]; + + _frames.clear (); + first_vala = null; + max_file_name_length = 0; + is_all_function_name_blank = true; + is_all_file_name_blank = true; + + #if VALA_0_26 + var size = Linux.Backtrace.@get (array); + var strings = Linux.Backtrace.symbols (array); + #else + int size = Linux.backtrace (array, frame_count); + unowned string[] strings = Linux.backtrace_symbols (array, size); + // Needed because of some weird bug + strings.length = size; + #endif + + int[] addresses = (int[])array; + string module = get_module_name (); + // First ones are the handler + for (int i = skipped_frames_count ; i < size ; i++) { + int address = addresses[i]; + string str = strings[i]; + var addr = extract_address (str); + + var full_line = process_line (module, addr); + process_info_for_file( full_line, str) ; + if (file_line == "") { + file_path = extract_file_path_from (str); + + } + if( file_path.has_suffix(".so.0")) { + process_info_from_lib (file_path, str) ; + } + + //stdout.printf ("Building %d \n . addr: [%s]\n . full_line: '%s'\n . file_line: '%s'\n . func_line: '%s'\n . str : '%s'\n . func: '%s'\n . file: '%s'\n . line: '%s'\n", + //i, addr, full_line, file_line, func_line, str, func, file_path, l); + + if (func != "" && file_path.has_suffix (".vala") && is_all_function_name_blank) + is_all_function_name_blank = false; + + if (short_file_path != "" && is_all_file_name_blank) + is_all_file_name_blank = false; + + var frame = new Frame (addr, file_line, func, file_path, short_file_path); + + if (first_vala == null && file_path.has_suffix (".vala")) + first_vala = frame; + + if (short_file_path.length > max_file_name_length) + max_file_name_length = short_file_path.length; + if (l.length > max_line_number_length) + max_line_number_length = l.length; + _frames.add (frame); + } + } + + private string extract_function_name (string line) { + if (line == "") + return ""; + var start = line.index_of ("("); + if (start >= 0) { + var end = line.index_of ("+", start); + if (end >= 0) { + var result = line.substring (start + 1, end - start - 1); + return result.strip (); + } + } + return ""; + } + + private string extract_function_name_from_line (string line) { + return line.strip (); + } + + private string extract_file_path_from (string str) { + if (str == "") + return ""; + /*if( str.index_of("??") >= 0) + //result = result.substring (4, line.length - 4 ); + stdout.printf ("ERR2?? : %s\n", str ) ; */ + var start = str.index_of ("("); + if (start >= 0) { + return str.substring (0, start).strip (); + } + return str.strip (); + } + + private string extract_file_path (string line) { + var result = line; + if (result == "") + return ""; + if (result == "??:0??:0") + return ""; + // For some reason, the file name can starts with ??:0 + if (result.has_prefix ("??:0")) + result = result.substring (4, line.length - 4); + // stdout.printf ("ERR1?? : %s\n", line ) ; + var start = result.index_of (":"); + if (start >= 0) { + result = result.substring (0, start); + return result.strip (); + } + return ""; + } + + public static string extract_line (string line) { + var result = line; + if (result == "") + return ""; + if (result.has_prefix ("??:0")) + result = result.substring (4, line.length - 4); + var start = result.index_of (":"); + if (start >= 0) { + result = result.substring (start + 1, line.length - start - 1); + var end = result.index_of ("("); + if (end >= 0) { + result = result.substring (0, end); + } + return result.strip (); + } + return ""; + } + + private string extract_address (string line) { + if (line == "") + return ""; + var start = line.index_of ("["); + if (start >= 0) { + var end = line.index_of ("]", start); + if (end >= 0) { + var result = line.substring (start + 1, end - start - 1); + return result.strip (); + } + } + return ""; + } + + private string execute_command_sync_get_output (string cmd) { + try { + int exitCode; + string std_out; + string std_err; + Process.spawn_command_line_sync (cmd, out std_out, out std_err, out exitCode); + return std_out; + } + catch (Error e) { + warning (@"Error while executing '$cmd': $(e.message)"); + return ""; + } + } + + // Poor's man demangler. libunwind is another dep + // TODO : Optimize this + // module : app + // address : 0x007f80 + // output : /home/cran/Projects/noise/noise-perf-instant-search/tests/errors.vala:87 + string process_line (string module, string address) { + var cmd = "addr2line -f -e %s %s".printf (module, address); + var result = execute_command_sync_get_output (cmd); + //stdout.printf( "CMD %s\n", cmd) ; + return result; + } + + private string get_reset_code () { + // return get_color_code (Style.RESET, Colors.WHITE, Colors.BLACK); + return "\x1b[0m"; + } + + private string get_reset_style () { + return get_color_code (Style.DIM, highlight_color, background_color); + } + + private string get_color_code (Style attr, Color fg, Color bg = background_color) { + /* Command is the control command to the terminal */ + if (bg == Color.BLACK) + return "%c[%d;%dm".printf (0x1B, (int) attr, (int) fg + 30); + else + return "%c[%d;%d;%dm".printf (0x1B, (int) attr, (int) fg + 30, (int) bg + 40); + } + + private string get_signal_name () { + return sig.to_string (); + } + + private string get_highlight_code () { + return get_color_code (Style.BRIGHT, highlight_color); + } + + private string get_printable_function (Frame frame, int padding = 0) { + var result = ""; + var is_unknown = false; + if (frame.function == "") { + result = " " + frame.address; + is_unknown = true; + } else { + var s = ""; + int count = padding - get_signal_name ().length; + if (padding != 0 && count > 0) + s = string.nfill (count, ' '); + result = "'" + frame.function + "'" + s; + } + if (is_unknown) + return result + get_reset_code (); + else + return get_highlight_code () + result + get_reset_code (); + } + + private string get_printable_line_number (Frame frame, bool pad = true) { + var path = frame.line_number; + var result = ""; + var color = get_highlight_code (); + if (path.length >= max_line_number_length || !pad) + result = color + path + get_reset_style (); + else { + result = color + path + get_reset_style (); + result = string.nfill (max_line_number_length - path.length, ' ') + result; + } + return result; + } + + private string get_printable_file_short_path (Frame frame, bool pad = true) { + var path = frame.file_short_path; + var result = ""; + var color = get_highlight_code (); + if (path.length >= max_file_name_length || !pad) + result = color + path + get_reset_style (); + else { + result = color + path + get_reset_style (); + result = result + string.nfill (max_file_name_length - path.length, ' '); + } + return result; + } + + Color background_color = Color.BLACK; + int title_length = 0; + + private string get_printable_title () { + var c = get_color_code (Style.DIM, highlight_color, background_color); + var color = get_highlight_code (); + + var result = "" ; + + if( is_custom) + result = "%sA function was called in %s".printf ( + c, + get_reset_style ()); + else + result = "%sAn error occured %s(%s)%s".printf ( + c, + color, + get_signal_name (), + get_reset_style ()); + + title_length = get_signal_name ().length; + return result; + } + + private string get_reason () { + // var c = get_reset_code(); + var color = get_highlight_code (); + if (sig == ProcessSignal.TRAP) { + return "The reason is likely %san uncaught error%s".printf ( + color, get_reset_code ()); + } + if (sig == ProcessSignal.ABRT) { + return "The reason is likely %sa failed assertion (assert...)%s".printf ( + color, get_reset_code ()); + } + if (sig == ProcessSignal.SEGV) { + return "The reason is likely %sa null reference being used%s".printf ( + color, get_reset_code ()); + } + return "Unknown reason"; + } + + public void print () { + stdout.printf ("\n"); + background_color = error_background; + var header = "%s%s\n".printf (get_printable_title (), + get_reset_code ()); + + if (first_vala != null) { + header = "%s in %s, line %s in %s\n".printf ( + get_printable_title (), + get_printable_file_short_path (first_vala, false), + get_printable_line_number (first_vala, false), + get_printable_function (first_vala) + get_reset_code ()); + title_length += first_vala.line_number.length + + first_vala.function.length + + first_vala.file_short_path.length; + } + stdout.printf (header); + background_color = Color.BLACK; + if( !is_custom) { + var reason = get_reason (); + stdout.printf ("%s\n", reason); + } + + // Has the user forgot to compile with -g -X -rdynamic flag ? + if (is_all_file_name_blank) { + //var advice = " %sNote%s: no file path and line numbers can be retrieved. Are you sure %syou added -g -X -rdynamic%s to valac command line?\n"; + var advice = "%sNote%s: no vala function name can be retrieved."; + var color = get_highlight_code (); + stdout.printf (advice, color, get_reset_code (), color, get_reset_code ()); + } + + // Has the user forgot to compile with rdynamic flag ? + if (is_all_function_name_blank && !is_all_file_name_blank) { + var advice = "%sNote%s: no vala function name can be retrieved."; + //var advice = " %sNote%s: no vala function name can be retrieved. Are you sure %syou added -X -rdynamic%s to valac command line?\n"; + var color = get_highlight_code (); + stdout.printf (advice, color, get_reset_code (), color, get_reset_code ()); + } + + stdout.printf ("\n"); + int i = 1; + bool has_displayed_first_vala = false; + foreach (var frame in _frames) { + var show_frame = frame.function != "" || frame.file_path.has_suffix (".vala") || frame.file_path.has_suffix (".c"); + if (hide_installed_libraries && has_displayed_first_vala) + show_frame = show_frame && frame.file_short_path != ""; + + // Ignore glib tracing code if displayed before the first vala frame + if ((frame.function == "g_logv" || frame.function == "g_log") && !has_displayed_first_vala) + show_frame = false; + if (show_frame) { + // #2 ./OtherModule.c line 80 in 'other_module_do_it' + // at /home/cran/Projects/noise/noise-perf-instant-search/tests/errors/module/OtherModule.vala:10 + var str = " %s #%d %s line %s in %s\n"; + background_color = Color.BLACK; + var lead = " "; + var function_padding = 0; + if (frame == first_vala) { + has_displayed_first_vala = true; + lead = "*"; + background_color = error_background; + function_padding = 22; + } + var l_number = ""; + if (frame.line_number == "") { + str = " %s #%d %s in %s\n"; + var func_name = get_printable_function (frame); + var fill_len = int.max (max_file_name_length + max_line_number_length - 1, 0); + str = str.printf ( + lead, + i, + string.nfill (fill_len, ' '), + func_name); + } else { + str = str.printf ( + lead, + i, + get_printable_file_short_path (frame), + get_printable_line_number (frame), + get_printable_function (frame, function_padding)); + l_number = ":" + frame.line_number; + } + stdout.printf (str); + str = " at %s%s\n".printf ( + frame.file_path, l_number); + stdout.printf (str); + + i++; + } + } + } + + public static void register_handlers () { + Process.@signal (ProcessSignal.SEGV, handler); + Process.@signal (ProcessSignal.ABRT, handler); + Process.@signal (ProcessSignal.TRAP, handler); + } + + public static CriticalHandler critical_handling { get;set;default = CriticalHandler.PRINT_STACKTRACE;} + + public static void handler (int sig) { + Stacktrace stack = new Stacktrace ((ProcessSignal) sig); + stack.print (); + if (sig != ProcessSignal.TRAP || + (sig == ProcessSignal.TRAP && critical_handling == CriticalHandler.CRASH)) + Process.exit (1); + } + +} + diff --git a/src/Utils.vala b/src/Utils.vala new file mode 100644 index 0000000..24571bf --- /dev/null +++ b/src/Utils.vala @@ -0,0 +1,16 @@ +public class Tootle.Utils { + + public static void merge (GLib.Object what, GLib.Object with) { + var props = with.get_class ().list_properties (); + foreach (var prop in props) { + var name = prop.get_name (); + var defined = what.get_class ().find_property (name) != null; + if (defined) { + var val = Value (prop.value_type); + with.get_property (name, ref val); + what.set_property (name, val) ; + } + } + } + +} diff --git a/src/Views/Abstract.vala b/src/Views/Abstract.vala deleted file mode 100644 index d7a398e..0000000 --- a/src/Views/Abstract.vala +++ /dev/null @@ -1,73 +0,0 @@ -using Gtk; - -public abstract class Tootle.Views.Abstract : ScrolledWindow { - - public bool current = false; - public int stack_pos = -1; - public Image? image; - public Box view; - protected Box? empty; - protected Grid? header; - - construct { - view = new Box (Orientation.VERTICAL, 0); - view.valign = Align.START; - add (view); - - hscrollbar_policy = PolicyType.NEVER; - edge_reached.connect (pos => { - if (pos == PositionType.BOTTOM) - on_bottom_reached (); - }); - } - - public Abstract () { - show_all (); - } - - public virtual string get_icon () { - return "null"; - } - - public virtual string get_name () { - return "unnamed"; - } - - public virtual void clear (){ - view.forall (widget => { - if (widget != header) - widget.destroy (); - }); - } - - public virtual void on_bottom_reached () {} - public virtual void on_set_current () {} - - public virtual bool is_empty () { - return view.get_children ().length () <= 1; - } - - public virtual bool empty_state () { - if (empty != null) - empty.destroy (); - if (!is_empty ()) - return false; - - empty = new Box (Orientation.VERTICAL, 0); - empty.margin = 64; - var image = new Image.from_resource ("/com/github/bleakgrey/tootle/empty_state"); - var text = new Label (_("Nothing to see here")); - text.get_style_context ().add_class ("h2"); - text.opacity = 0.5; - empty.hexpand = true; - empty.vexpand = true; - empty.valign = Align.FILL; - empty.pack_start (image, false, false, 0); - empty.pack_start (text, false, false, 12); - empty.show_all (); - view.pack_start (empty, false, false, 0); - - return true; - } - -} diff --git a/src/Views/Base.vala b/src/Views/Base.vala new file mode 100644 index 0000000..d6cdd09 --- /dev/null +++ b/src/Views/Base.vala @@ -0,0 +1,89 @@ +using Gtk; + +[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/views/base.ui")] +public class Tootle.Views.Base : Box { + + public static string STATUS_EMPTY = _("Nothing to see here"); + public static string STATUS_LOADING = " "; + + public bool current = false; + public int stack_pos = -1; + public Image? image; + + [GtkChild] + protected ScrolledWindow scrolled; + [GtkChild] + protected Box view; + [GtkChild] + protected Stack states; + [GtkChild] + protected Box content; + [GtkChild] + private Label status_message_label; + [GtkChild] + protected Button status_button; + [GtkChild] + private Stack status_stack; + + public string state { get; set; default = "status"; } + public string status_message { get; set; default = STATUS_EMPTY; } + public bool allow_closing { get; set; default = true; } + + public bool empty { + get { + return content.get_children ().length () <= 0; + } + } + + construct { + status_button.label = _("Reload"); + bind_property ("state", states, "visible-child-name", BindingFlags.SYNC_CREATE); + scrolled.edge_reached.connect (pos => { + if (pos == PositionType.BOTTOM) + on_bottom_reached (); + }); + content.remove.connect (() => on_content_changed ()); + + notify["status-message"].connect (() => { + status_message_label.label = @"$status_message"; + status_stack.visible_child_name = status_message == STATUS_LOADING ? "spinner" : "message"; + }); + } + + public virtual string get_icon () { + return "null"; + } + + public virtual string get_name () { + return "unnamed"; + } + + public virtual void clear (){ + content.forall (widget => { + widget.destroy (); + }); + state = "status"; + } + + public virtual void on_bottom_reached () {} + public virtual void on_set_current () {} + + public virtual void on_content_changed () { + if (empty) { + status_message = STATUS_EMPTY; + state = "status"; + } + else { + state = "content"; + } + check_resize (); + } + + public virtual void on_error (int32 code, string reason) { + status_message = reason; + status_button.visible = true; + status_button.sensitive = true; + state = "status"; + } + +} diff --git a/src/Views/Direct.vala b/src/Views/Direct.vala index 1f8b04e..165e730 100644 --- a/src/Views/Direct.vala +++ b/src/Views/Direct.vala @@ -1,20 +1,19 @@ public class Tootle.Views.Direct : Views.Timeline { public Direct () { - base ("direct"); + Object (timeline: "direct"); } - + public override string get_icon () { return "mail-send-symbolic"; } - + public override string get_name () { return _("Direct Messages"); } - - public override Soup.Message? get_stream () { - var url = "%s/api/v1/streaming/?stream=direct&access_token=%s".printf (accounts.formal.instance, accounts.formal.token); - return new Soup.Message("GET", url); + + public override string? get_stream_url () { + return @"/api/v1/streaming/?stream=direct&access_token=$(accounts.active.token)"; } } diff --git a/src/Views/ExpandedStatus.vala b/src/Views/ExpandedStatus.vala index e195ca9..02103ad 100644 --- a/src/Views/ExpandedStatus.vala +++ b/src/Views/ExpandedStatus.vala @@ -1,111 +1,91 @@ using Gtk; -public class Tootle.Views.ExpandedStatus : Views.Abstract { +public class Tootle.Views.ExpandedStatus : Views.Base, IAccountListener { - private API.Status root_status; - private bool last_status_was_root = false; - private bool sensitive_visible = false; + public API.Status root_status { get; construct set; } + protected InstanceAccount? account = null; + protected Widgets.Status root_widget; public ExpandedStatus (API.Status status) { - base (); - root_status = status; + Object (root_status: status, state: "content"); + + root_widget = append (status); + root_widget.avatar.button_press_event.connect (root_widget.on_avatar_clicked); + root_widget.get_style_context ().add_class ("card"); + root_widget.get_style_context ().add_class ("highlight"); + + connect_account (); + } + + public override void on_account_changed (InstanceAccount? acc) { + account = acc; request (); - - window.button_reveal.clicked.connect (on_reveal_toggle); } - ~ExpandedStatus () { - if (window != null) { - window.button_reveal.clicked.disconnect (on_reveal_toggle); - window.button_reveal.hide (); - } - } - - private void prepend (API.Status status, bool is_root = false){ - var separator = new Separator (Orientation.HORIZONTAL); - separator.show (); - + private Widgets.Status prepend (API.Status status, bool to_end = false){ var widget = new Widgets.Status (status); widget.avatar.button_press_event.connect (widget.on_avatar_clicked); - if (!is_root) - widget.button_press_event.connect (widget.open); - else - widget.highlight (); + widget.revealer.reveal_child = true; - if (!last_status_was_root) { - widget.separator = separator; - view.pack_start (separator, false, false, 0); - } - view.pack_start (widget, false, false, 0); - last_status_was_root = is_root; + content.pack_start (widget, false, false, 0); + if (!to_end) + content.reorder_child (widget, 0); - if (status.has_spoiler ()) - window.button_reveal.show (); - if (sensitive_visible) - reveal_sensitive (widget); + check_resize (); + return widget; + } + private Widgets.Status append (API.Status status) { + return prepend (status, true); } - public Soup.Message request (){ - var url = "%s/api/v1/statuses/%lld/context".printf (accounts.formal.instance, root_status.id); - var msg = new Soup.Message ("GET", url); - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, mess) => { - var root = network.parse (mess); - var ancestors = root.get_array_member ("ancestors"); - ancestors.foreach_element ((array, i, node) => { - var object = node.get_object (); - if (object != null) { - var status = API.Status.parse (object); - prepend (status); + public void request () { + new Request.GET (@"/api/v1/statuses/$(root_status.id)/context") + .with_account (account) + .then_parse_obj (root => { + if (scrolled == null) return; + + var ancestors = root.get_array_member ("ancestors"); + ancestors.foreach_element ((array, i, node) => { + var object = node.get_object (); + if (object != null) { + var status = new API.Status (object); + prepend (status); + } + }); + + var descendants = root.get_array_member ("descendants"); + descendants.foreach_element ((array, i, node) => { + var object = node.get_object (); + if (object != null) { + var status = new API.Status (object); + append (status); + } + }); + + int x,y; + translate_coordinates (root_widget, 0, 0, out x, out y); + scrolled.vadjustment.value = (double)(y*-1); //TODO: Animate scrolling? + }) + .exec (); + } + + public static void open_from_link (string q) { + new Request.GET ("/api/v1/search") + .with_account () + .with_param ("q", q) + .with_param ("resolve", "true") + .then ((sess, msg) => { + var root = network.parse (msg); + var statuses = root.get_array_member ("statuses"); + var object = statuses.get_element (0).get_object (); + if (object != null){ + var status = new API.Status (object); + window.open_view (new Views.ExpandedStatus (status)); } - }); - - prepend (root_status, true); - - var descendants = root.get_array_member ("descendants"); - descendants.foreach_element ((array, i, node) => { - var object = node.get_object (); - if (object != null) { - var status = API.Status.parse (object); - prepend (status); - } - }); - }); - return msg; - } - - public static void open_from_link (string q){ - var url = "%s/api/v1/search?q=%s&resolve=true".printf (accounts.formal.instance, q); - var msg = new Soup.Message ("GET", url); - msg.priority = Soup.MessagePriority.HIGH; - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, mess) => { - var root = network.parse (mess); - var statuses = root.get_array_member ("statuses"); - var object = statuses.get_element (0).get_object (); - if (object != null){ - var st = API.Status.parse (object); - window.open_view (new Views.ExpandedStatus (st)); - } - else - Desktop.open_uri (q); - }); - } - - private void on_reveal_toggle () { - sensitive_visible = !sensitive_visible; - view.forall (w => { - if (!(w is Widgets.Status)) - return; - - var widget = w as Widgets.Status; - reveal_sensitive (widget); - }); - } - - private void reveal_sensitive (Widgets.Status widget) { - if (widget.status.has_spoiler ()) - widget.revealer.reveal_child = sensitive_visible; + else + Desktop.open_uri (q); + }) + .exec (); } } diff --git a/src/Views/Favorites.vala b/src/Views/Favorites.vala index f74479e..96378de 100644 --- a/src/Views/Favorites.vala +++ b/src/Views/Favorites.vala @@ -1,15 +1,14 @@ public class Tootle.Views.Favorites : Views.Timeline { public Favorites () { - base ("favorites"); + Object (timeline: "favorites"); } - + public override string get_url (){ if (page_next != null) return page_next; - - var url = "%s/api/v1/favourites/?limit=%i".printf (accounts.formal.instance, this.limit); - return url; + + return @"/api/v1/favourites"; } } diff --git a/src/Views/Federated.vala b/src/Views/Federated.vala index 3b46071..dc02ea3 100644 --- a/src/Views/Federated.vala +++ b/src/Views/Federated.vala @@ -1,24 +1,19 @@ public class Tootle.Views.Federated : Views.Timeline { public Federated () { - base ("public"); + Object (timeline: "public", is_public: true); } - + public override string get_icon () { return "network-workgroup-symbolic"; } - + public override string get_name () { return _("Federated Timeline"); } - - protected override bool is_public () { - return true; - } - - public override Soup.Message? get_stream () { - var url = "%s/api/v1/streaming/?stream=public&access_token=%s".printf (accounts.formal.instance, accounts.formal.token); - return new Soup.Message("GET", url); + + public override string? get_stream_url () { + return account != null ? @"$(account.instance)/api/v1/streaming/?stream=public&access_token=$(account.token)" : null; } } diff --git a/src/Views/Followers.vala b/src/Views/Followers.vala deleted file mode 100644 index dc8107f..0000000 --- a/src/Views/Followers.vala +++ /dev/null @@ -1,52 +0,0 @@ -using Gtk; - -public class Tootle.Views.Followers : Views.Timeline { - - public Followers (API.Account account) { - base (account.id.to_string ()); - } - - public new void append (API.Account account){ - if (empty != null) - empty.destroy (); - - var separator = new Separator (Orientation.HORIZONTAL); - separator.show (); - - var widget = new Widgets.Account (account); - widget.separator = separator; - view.pack_start (separator, false, false, 0); - view.pack_start (widget, false, false, 0); - } - - public override string get_url (){ - if (page_next != null) - return page_next; - - var url = "%s/api/v1/accounts/%s/followers".printf (accounts.formal.instance, this.timeline); - return url; - } - - public override void request (){ - var msg = new Soup.Message("GET", get_url ()); - msg.finished.connect (() => empty_state ()); - network.queue (msg, (sess, mess) => { - try { - network.parse_array (mess).foreach_element ((array, i, node) => { - var object = node.get_object (); - if (object != null){ - var status = API.Account.parse (object); - append (status); - } - }); - - get_pages (mess.response_headers.get_one ("Link")); - } - catch (GLib.Error e) { - warning ("Can't get account follow info:"); - warning (e.message); - } - }); - } - -} diff --git a/src/Views/Following.vala b/src/Views/Following.vala deleted file mode 100644 index fa2ecda..0000000 --- a/src/Views/Following.vala +++ /dev/null @@ -1,16 +0,0 @@ -public class Tootle.Views.Following : Views.Followers { - - public Following (API.Account account) { - base (account); - - } - - public override string get_url (){ - if (page_next != null) - return page_next; - - var url = "%s/api/v1/accounts/%s/following".printf (accounts.formal.instance, this.timeline); - return url; - } - -} diff --git a/src/Views/Hashtag.vala b/src/Views/Hashtag.vala index 5ea2097..15d91ef 100644 --- a/src/Views/Hashtag.vala +++ b/src/Views/Hashtag.vala @@ -1,20 +1,12 @@ public class Tootle.Views.Hashtag : Views.Timeline { - public Hashtag (string hashtag) { - base ("tag/" + hashtag); + public Hashtag (string tag) { + Object (timeline: @"tag/$tag"); } - - public string get_hashtag () { - return this.timeline.substring (4); - } - - public override string get_name () { - return get_hashtag (); - } - - public override Soup.Message? get_stream () { - var url = "%s/api/v1/streaming/?stream=hashtag&tag=%s&access_token=%s".printf (accounts.formal.instance, get_hashtag (), accounts.formal.token); - return new Soup.Message("GET", url); + + public override string? get_stream_url () { + var tag = timeline.substring (4); + return account != null ? @"$(account.instance)/api/v1/streaming/?stream=hashtag&tag=$tag&access_token=$(account.token)" : null; } } diff --git a/src/Views/Home.vala b/src/Views/Home.vala index 36d45df..1b23709 100644 --- a/src/Views/Home.vala +++ b/src/Views/Home.vala @@ -1,7 +1,7 @@ public class Tootle.Views.Home : Views.Timeline { public Home () { - base ("home"); + Object (timeline: "home"); } public override string get_icon () { @@ -12,8 +12,8 @@ public class Tootle.Views.Home : Views.Timeline { return _("Home"); } - public override Soup.Message? get_stream () { - return accounts.formal.get_stream (); + public override string? get_stream_url () { + return account.get_stream_url () ?? null; } } diff --git a/src/Views/Local.vala b/src/Views/Local.vala index 020789c..75abd71 100644 --- a/src/Views/Local.vala +++ b/src/Views/Local.vala @@ -1,8 +1,4 @@ -public class Tootle.Views.Local : Views.Timeline { - - public Local () { - base ("public"); - } +public class Tootle.Views.Local : Views.Federated { public override string get_icon () { return Desktop.fallback_icon ("system-users-symbolic", "document-open-recent-symbolic"); @@ -12,19 +8,14 @@ public class Tootle.Views.Local : Views.Timeline { return _("Local Timeline"); } - public override string get_url (){ - var url = base.get_url (); - url += "&local=true"; - return url; + public override Request append_params (Request req) { + req.with_param ("local", "true"); + req.with_param ("limit", limit.to_string ()); + return req; } - protected override bool is_public () { - return true; - } - - public override Soup.Message? get_stream () { - var url = "%s/api/v1/streaming/?stream=public:local&access_token=%s".printf (accounts.formal.instance, accounts.formal.token); - return new Soup.Message("GET", url); + public override string? get_stream_url () { + return account != null ? @"$(account.instance)/api/v1/streaming/?stream=public:local&access_token=$(account.token)" : null; } } diff --git a/src/Views/NewAccount.vala b/src/Views/NewAccount.vala new file mode 100644 index 0000000..49e2a01 --- /dev/null +++ b/src/Views/NewAccount.vala @@ -0,0 +1,183 @@ +using Gtk; + +public class Tootle.Views.NewAccount : Views.Base { + + private string? instance { get; set; } + private string? code { get; set; } + private string scopes = "read%20write%20follow"; + + private string? client_id { get; set; } + private string? client_secret { get; set; } + private string? access_token { get; set; } + private string redirect_uri { get; set; default = "urn:ietf:wg:oauth:2.0:oob"; } //TODO: Investigate URI handling for automatic token getting + private InstanceAccount account; + + private Button next_button; + private Entry instance_entry; + private Entry code_entry; + private Label reset_label; + + private Stack stack; + private Widget step1; + private Widget step2; + + public NewAccount (bool allow_closing = true) { + Object (allow_closing: allow_closing); + + var builder = new Builder.from_resource (@"$(Build.RESOURCES)ui/views/new_account.ui"); + content.pack_start (builder.get_object ("wizard") as Grid); + state = "content"; + next_button = builder.get_object ("next") as Button; + reset_label = builder.get_object ("reset") as Label; + instance_entry = builder.get_object ("instance_entry") as Entry; + code_entry = builder.get_object ("code_entry") as Entry; + + stack = builder.get_object ("stack") as Stack; + step1 = builder.get_object ("step1") as Widget; + step2 = builder.get_object ("step2") as Widget; + + next_button.clicked.connect (on_next_clicked); + reset_label.activate_link.connect (reset); + instance_entry.text = "https://mastodon.social/"; //TODO: REMOVE ME + info ("New account view was requested"); + } + + private bool reset () { + info ("State invalidated"); + instance = code = client_id = client_secret = access_token = null; + instance_entry.sensitive = true; + stack.visible_child = step1; + return true; + } + + private void oopsie (string message) { + warning (message); + } + + private void on_next_clicked () { + try { + step (); + } + catch (Oopsie e) { + oopsie (e.message); + } + } + + private void step () throws Error { + if (instance == null) + setup_instance (); + + if (client_secret == null || client_id == null) { + register_client (); + return; + } + + code = code_entry.text; + request_token (); + } + + private void setup_instance () throws Error { + info ("Checking instance URL"); + + var str = instance_entry.text + .replace ("/", "") + .replace (":", "") + .replace ("https", "") + .replace ("http", ""); + instance = "https://"+str; + instance_entry.text = str; + + if (str.char_count () <= 0 || !("." in instance)) + throw new Oopsie.USER (_("Instance URL is invalid")); + } + + private void register_client () throws Error { + info ("Registering client"); + instance_entry.sensitive = false; + + account = new InstanceAccount.empty (instance); + + new Request.POST (@"/api/v1/apps") + .with_param ("client_name", Build.NAME) + .with_param ("website", Build.WEBSITE) + .with_param ("scopes", scopes) + .with_param ("redirect_uris", redirect_uri) + .with_account (account) + .then ((sess, msg) => { + var root = network.parse (msg); + client_id = root.get_string_member ("client_id"); + client_secret = root.get_string_member ("client_secret"); + info ("OK: instance registered client"); + stack.visible_child = step2; + + open_confirmation_page (); + }) + .on_error ((status, reason) => { + oopsie (reason); + instance_entry.sensitive = true; + }) + .exec (); + } + + private void open_confirmation_page () { + info ("Opening permission request page"); + + var pars = @"scope=$scopes&response_type=code&redirect_uri=$redirect_uri&client_id=$client_id"; + var url = @"$instance/oauth/authorize?$pars"; + Desktop.open_uri (url); + } + + private void request_token () throws Error { + if (code.char_count () <= 10) + throw new Oopsie.USER (_("Please paste a valid authorization code")); + + info ("Requesting access token"); + new Request.POST (@"/oauth/token") + .with_account (account) + .with_param ("client_id", client_id) + .with_param ("client_secret", client_secret) + .with_param ("redirect_uri", redirect_uri) + .with_param ("grant_type", "authorization_code") + .with_param ("code", code) + .then ((sess, msg) => { + var root = network.parse (msg); + access_token = root.get_string_member ("access_token"); + account.token = access_token; + account.id = 0; + info ("OK: received access token"); + request_profile (); + }) + .on_error ((code, reason) => oopsie (reason)) + .exec (); + } + + private void request_profile () throws Error { + info ("Testing received access token"); + new Request.GET ("/api/v1/accounts/verify_credentials") + .with_account (account) + .then ((sess, msg) => { + var root = network.parse (msg); + var account = new API.Account (root); + info ("OK: received user profile"); + save (account); + }) + .on_error ((status, reason) => { + reset (); + oopsie (reason); + }) + .exec (); + } + + private void save (API.Account profile) { + info ("Account validated. Saving..."); + account.patch (profile); + account.instance = instance; + account.client_id = client_id; + account.client_secret = client_secret; + account.token = access_token; + accounts.add (account); + + destroy (); + } + +} diff --git a/src/Views/Notifications.vala b/src/Views/Notifications.vala index 94aa9b6..d470c22 100644 --- a/src/Views/Notifications.vala +++ b/src/Views/Notifications.vala @@ -1,23 +1,20 @@ using Gtk; using Gdk; -public class Tootle.Views.Notifications : Views.Abstract { +public class Tootle.Views.Notifications : Views.Base, IAccountListener { - private int64 last_id = 0; - private bool force_dot = false; + protected InstanceAccount? account = null; + protected int64 last_id = 0; + protected bool force_dot = false; public Notifications () { - base (); - view.remove.connect (on_remove); - accounts.switched.connect (on_account_changed); app.refresh.connect (on_refresh); - network.notification.connect (prepend); - - request (); + status_button.clicked.connect (on_refresh); + streams.notification.connect (prepend); + connect_account (); } private bool has_unread () { - var account = accounts.formal; if (account == null) return false; return last_id > account.last_seen_notification || force_dot; @@ -39,38 +36,33 @@ public class Tootle.Views.Notifications : Views.Abstract { } public void append (API.Notification notification, bool reverse = false) { - if (empty != null) - empty.destroy (); + GLib.Idle.add (() => { + var widget = new Widgets.Notification (notification); + content.pack_start (widget, false, false, 0); - var separator = new Separator (Orientation.HORIZONTAL); - separator.show (); + if (reverse) { + content.reorder_child (widget, 0); - var widget = new Widgets.Notification (notification); - widget.separator = separator; - view.pack_start (separator, false, false, 0); - view.pack_start (widget, false, false, 0); - - if (reverse) { - view.reorder_child (widget, 0); - view.reorder_child (separator, 0); - - if (!current) { - force_dot = true; - accounts.formal.has_unread_notifications = force_dot; + if (!current) { + force_dot = true; + accounts.active.has_unread_notifications = force_dot; + } } - } - if (notification.id > last_id) - last_id = notification.id; + on_content_changed (); - if (has_unread ()) { - accounts.save (); - image.icon_name = get_icon (); - } + if (notification.id > last_id) + last_id = notification.id; + + if (has_unread ()) { + accounts.save (); + image.icon_name = get_icon (); + } + return GLib.Source.REMOVE; + }); } public override void on_set_current () { - var account = accounts.formal; if (has_unread ()) { force_dot = false; account.has_unread_notifications = force_dot; @@ -80,73 +72,58 @@ public class Tootle.Views.Notifications : Views.Abstract { } } - public virtual void on_remove (Widget widget) { - if (!(widget is Widgets.Notification)) - return; - - empty_state (); - } - - public override bool empty_state () { - var is_empty = base.empty_state (); - if (image != null && is_empty) + public override void on_content_changed () { + base.on_content_changed (); + if (image != null && empty) image.icon_name = get_icon (); - - return is_empty; } public virtual void on_refresh () { clear (); - request (); + GLib.Idle.add (request); } - public virtual void on_account_changed (API.Account? account) { - if (account == null) - return; - - last_id = accounts.formal.last_seen_notification; - force_dot = accounts.formal.has_unread_notifications; - on_refresh (); + public virtual void on_account_changed (InstanceAccount? acc) { + account = acc; + if (account == null) { + last_id = 0; + force_dot = false; + } + else { + last_id = account.last_seen_notification; + force_dot = account.has_unread_notifications; + } + on_refresh (); } - public void request () { - if (accounts.current == null) { - empty_state (); - return; + public bool request () { + if (account != null) { + account.cached_notifications.@foreach (notification => { + append (notification); + return true; + }); } - accounts.formal.cached_notifications.@foreach (notification => { - append (notification); - return true; - }); + // new Request.GET ("/api/v1/follow_requests") //TODO: this + // .with_account () + // .then_parse_array (node => { + // var notification = API.Notification.parse_follow_request (node.get_object ()); + // append (notification); + // }) + // .on_error (on_error) + // .exec (); - var url = "%s/api/v1/follow_requests".printf (accounts.formal.instance); - var msg = new Soup.Message ("GET", url); - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, mess) => { - network.parse_array (mess).foreach_element ((array, i, node) => { - var obj = node.get_object (); - if (obj != null){ - var notification = API.Notification.parse_follow_request (obj); - append (notification); - } - }); - }); + new Request.GET ("/api/v1/notifications") + .with_account (account) + .with_param ("limit", "30") + .then_parse_array (node => { + var notification = new API.Notification (node.get_object ()); + append (notification); + }) + .on_error (on_error) + .exec (); - var url2 = "%s/api/v1/notifications?limit=30".printf (accounts.formal.instance); - var msg2 = new Soup.Message ("GET", url2); - network.inject (msg2, Network.INJECT_TOKEN); - network.queue (msg2, (sess, mess) => { - network.parse_array (mess).foreach_element ((array, i, node) => { - var obj = node.get_object (); - if (obj != null){ - var notification = API.Notification.parse (obj); - append (notification); - } - }); - }); - - empty_state (); + return GLib.Source.REMOVE; } } diff --git a/src/Views/Profile.vala b/src/Views/Profile.vala index 09b8ac7..d23013e 100644 --- a/src/Views/Profile.vala +++ b/src/Views/Profile.vala @@ -1,249 +1,190 @@ using Gtk; -using Granite; public class Tootle.Views.Profile : Views.Timeline { - const int AVATAR_SIZE = 128; - protected API.Account account; + public API.Account profile { get; construct set; } + + protected RadioButton filter_all; + protected RadioButton filter_replies; + protected RadioButton filter_media; - protected Grid header_image; - protected Box header_info; - protected Granite.Widgets.Avatar avatar; - protected Widgets.RichLabel display_name; - protected Label username; protected Label relationship; - protected Widgets.RichLabel note; - protected Grid counters; protected Box actions; - protected Button button_follow; - - protected Gtk.Menu menu; - protected Gtk.MenuItem menu_edit; - protected Gtk.MenuItem menu_mention; - protected Gtk.MenuItem menu_mute; - protected Gtk.MenuItem menu_block; - protected Gtk.MenuItem menu_report; - protected Gtk.MenuButton button_menu; + protected Button follow_button; + protected MenuButton options_button; + protected Label posts_label; + protected Label following_label; + protected Label followers_label; + protected RadioButton posts_tab; + protected RadioButton following_tab; + protected RadioButton followers_tab; construct { - header = new Grid (); - header_info = new Box (Orientation.VERTICAL, 0); - header_info.margin = 12; - actions = new Box (Orientation.HORIZONTAL, 0); - actions.hexpand = false; - actions.halign = Align.END; - actions.vexpand = false; - actions.valign = Align.START; - actions.margin = 12; + profile.notify["rs"].connect (on_rs_updated); - relationship = new Label (""); - relationship.get_style_context ().add_class ("relationship"); - relationship.halign = Align.START; - relationship.valign = Align.START; - relationship.margin = 12; - header.attach (relationship, 0, 0, 1, 1); + var builder = new Builder.from_resource (@"$(Build.RESOURCES)ui/views/profile_header.ui"); + view.pack_start (builder.get_object ("grid") as Grid, false, false, 0); - avatar = new Granite.Widgets.Avatar.with_default_icon (AVATAR_SIZE); - avatar.hexpand = true; - avatar.margin_bottom = 6; - header_info.pack_start (avatar, false, false, 0); + var avatar = builder.get_object ("avatar") as Widgets.Avatar; + avatar.url = profile.avatar; - display_name = new Widgets.RichLabel (""); - display_name.get_style_context ().add_class (Granite.STYLE_CLASS_H2_LABEL); - header_info.pack_start (display_name, false, false, 0); + var name = builder.get_object ("name") as Widgets.RichLabel; + profile.bind_property ("display-name", name, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + var label = (string) src; + target.set_string (@"$label"); + return true; + }); - username = new Label (""); - header_info.pack_start (username, false, false, 0); + var handle = builder.get_object ("handle") as Widgets.RichLabel; + profile.bind_property ("acct", handle, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + target.set_string ("@" + (string) src); + return true; + }); - note = new Widgets.RichLabel (""); - note.set_line_wrap (true); - note.selectable = true; - note.margin_top = 12; - note.can_focus = false; - note.justify = Justification.CENTER; - header_info.pack_start (note, false, false, 0); - header_info.show_all (); - header.attach (header_info, 0, 0, 1, 1); + var note = builder.get_object ("note") as Widgets.RichLabel; + profile.bind_property ("note", note, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + target.set_string (Html.simplify ((string) src)); + return true; + }); - counters = new Grid (); - counters.column_homogeneous = true; - counters.get_style_context ().add_class ("header-counters"); - header.attach (counters, 0, 1, 1, 1); + actions = builder.get_object ("actions") as Box; + follow_button = builder.get_object ("follow_button") as Button; + follow_button.clicked.connect (on_follow_button_clicked); + options_button = builder.get_object ("options_button") as MenuButton; + relationship = builder.get_object ("relationship") as Label; - header_image = new Grid (); - header_image.get_style_context ().add_class ("header"); - header.attach (header_image, 0, 0, 2, 2); + posts_label = builder.get_object ("posts_label") as Label; + profile.bind_property ("posts_count", posts_label, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + var val = (int64) src; + target.set_string (_("%s Posts").printf (@"$val")); + return true; + }); + following_label = builder.get_object ("following_label") as Label; + profile.bind_property ("following_count", following_label, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + var val = (int64) src; + target.set_string (_("%s Follows").printf (@"$val")); + return true; + }); + followers_label = builder.get_object ("followers_label") as Label; + profile.bind_property ("followers_count", followers_label, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + var val = (int64) src; + target.set_string (_("%s Followers").printf (@"$val")); + return true; + }); - menu = new Gtk.Menu (); - menu_edit = new Gtk.MenuItem.with_label (_("Edit Profile")); - menu_mention = new Gtk.MenuItem.with_label (_("Mention")); - menu_report = new Gtk.MenuItem.with_label (_("Report")); - menu_mute = new Gtk.MenuItem.with_label (_("Mute")); - menu_block = new Gtk.MenuItem.with_label (_("Block")); - menu.add (menu_mention); - //menu.add (new Gtk.SeparatorMenuItem ()); - menu.add (menu_mute); - menu.add (menu_block); - //menu.add (menu_report); //TODO: Report users - //menu.add (menu_edit); //TODO: Edit profile - menu.show_all (); + filter_all = builder.get_object ("filter_all") as RadioButton; + filter_all.toggled.connect (on_refresh); + filter_replies = builder.get_object ("filter_replies") as RadioButton; + filter_replies.toggled.connect (on_refresh); + filter_media = builder.get_object ("filter_media") as RadioButton; + filter_media.toggled.connect (on_refresh); - button_follow = add_counter ("contact-new-symbolic"); - button_menu = new MenuButton (); - button_menu.image = new Image.from_icon_name ("view-more-symbolic", IconSize.LARGE_TOOLBAR); - button_menu.tooltip_text = _("More Actions"); - button_menu.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - (button_menu as Widget).set_focus_on_click (false); - button_menu.can_default = false; - button_menu.can_focus = false; - button_menu.popup = menu; - actions.pack_end(button_menu, false, false, 0); - actions.pack_end(button_follow, false, false, 0); - button_menu.hide (); - button_follow.hide (); - header.attach (actions, 0, 0, 2, 2); - - view.pack_start (header, false, false, 0); + posts_tab = builder.get_object ("posts_tab") as RadioButton; + posts_tab.toggled.connect (() => { + if (posts_tab.active) on_refresh (); + }); + following_tab = builder.get_object ("following_tab") as RadioButton; + following_tab.toggled.connect (() => { + if (following_tab.active) on_refresh (); + }); + followers_tab = builder.get_object ("followers_tab") as RadioButton; + followers_tab.toggled.connect (() => { + if (followers_tab.active) on_refresh (); + }); } public Profile (API.Account acc) { - base (""); - account = acc; - account.updated.connect (rebind); - - add_counter (_("Toots"), 1, account.statuses_count); - add_counter (_("Follows"), 2, account.following_count).clicked.connect (() => { - var view = new Views.Following (account); - window.open_view (view); - }); - add_counter (_("Followers"), 3, account.followers_count).clicked.connect (() => { - var view = new Views.Followers (account); - window.open_view (view); - }); - - show_all (); - - //TODO: Has this thing always been synchronous??? - //var stylesheet = ".header{background-image: url(\"%s\")}".printf (account.header); - //var css_provider = Granite.Widgets.Utils.get_css_provider (stylesheet); - //header_image.get_style_context ().add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - - menu_mention.activate.connect (() => Dialogs.Compose.open ("@%s ".printf (account.acct))); - menu_mute.activate.connect (() => account.set_muted (!account.rs.muting)); - menu_block.activate.connect (() => account.set_blocked (!account.rs.blocking)); - button_follow.clicked.connect (() => account.set_following (!account.rs.following)); - - rebind (); - account.get_relationship (); - request (); + Object (profile: acc); + profile.get_relationship (); } + protected void on_follow_button_clicked () { + actions.sensitive = false; + profile.set_following (!profile.rs.following); + } + protected void on_rs_updated () { + var rs = profile.rs; + var label = ""; + if (actions.sensitive = rs != null) { + if (rs.requested) + label = _("Sent follow request"); + else if (rs.followed_by && rs.following) + label = _("Mutually follows you"); + else if (rs.followed_by) + label = _("Follows you"); - public void rebind (){ - display_name.set_label ("%s".printf (account.display_name)); - username.label = "@" + account.acct; - note.set_label (account.note); - button_follow.visible = !account.is_self (); - network.load_avatar (account.avatar, avatar, 128); + foreach (Widget w in new Widget[] { follow_button, options_button }) { + var ctx = w.get_style_context (); + ctx.remove_class (STYLE_CLASS_SUGGESTED_ACTION); + ctx.remove_class (STYLE_CLASS_DESTRUCTIVE_ACTION); + ctx.add_class (rs.following ? STYLE_CLASS_DESTRUCTIVE_ACTION : STYLE_CLASS_SUGGESTED_ACTION); + } - menu_edit.visible = account.is_self (); + var label2 = ""; + if (rs.followed_by && !rs.following) + label2 = _("Follow back"); + else if (rs.following) + label2 = _("Unfollow"); + else + label2 = _("Follow"); - if (account.rs != null && !account.is_self ()) { - button_follow.show (); - if (account.rs.following) { - button_follow.tooltip_text = _("Unfollow"); - (button_follow.get_image () as Image).icon_name = "close-symbolic"; - } - else{ - button_follow.tooltip_text = _("Follow"); - (button_follow.get_image () as Image).icon_name = "contact-new-symbolic"; - } - } + follow_button.label = label2; + } - if (account.rs != null){ - button_menu.show (); - menu_block.label = account.rs.blocking ? _("Unblock") : _("Block"); - menu_mute.label = account.rs.muting ? _("Unmute") : _("Mute"); - menu_report.visible = menu_mute.visible = menu_block.visible = !account.is_self (); - - var rs_label = get_relationship_label (); - if (rs_label != null) { - relationship.label = rs_label; - relationship.show (); - } - else - relationship.hide (); - } - else - relationship.hide (); - } - - public override bool is_status_owned (API.Status status) { - return status.is_owned (); - } - - private Button add_counter (string name, int? i = null, int64? val = null) { - Button btn; - if (val != null){ - btn = new Button (); - var label = new Label ("%s\n%s".printf (name.up (), val.to_string ())); - label.justify = Justification.CENTER; - label.use_markup = true; - label.margin = 8; - btn.add (label); - } - else - btn = new Button.from_icon_name (name, IconSize.LARGE_TOOLBAR); - - btn.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - (btn as Widget).set_focus_on_click (false); - btn.can_default = false; - btn.can_focus = false; - - if (i != null) - counters.attach (btn, i, 1, 1, 1); - return btn; - } - - public override bool is_empty () { - return view.get_children ().length () <= 2; - } + relationship.label = label; + } public override string get_url () { if (page_next != null) return page_next; - var url = "%s/api/v1/accounts/%lld/statuses?limit=%i".printf (accounts.formal.instance, account.id, this.limit); - return url; + if (following_tab.active) + return @"/api/v1/accounts/$(profile.id)/following"; + else if (followers_tab.active) + return @"/api/v1/accounts/$(profile.id)/followers"; + else + return @"/api/v1/accounts/$(profile.id)/statuses"; } - public override void request () { - if (account != null) - base.request (); - } + public override Request append_params (Request req) { + req.with_param ("exclude_replies", (!filter_replies.active).to_string ()); + req.with_param ("only_media", filter_media.active.to_string ()); + return base.append_params (req); + } - private string? get_relationship_label () { - if (account.rs.requested) - return _("Sent follow request"); - else if (account.rs.blocking) - return _("Blocked"); - else if (account.rs.followed_by) - return _("Follows you"); - else if (account.rs.domain_blocking) - return _("Blocking this instance"); - else - return null; + public override bool request () { + append_params (new Request.GET (get_url ())) + .with_account (account) + .then_parse_array ((node, msg) => { + var obj = node.get_object (); + if (obj != null) { + API.Status status; + if (posts_tab.active) + status = new API.Status (obj); + else { + var account = new API.Account (obj); + status = new API.Status.from_account (account); + } + + append (status); + } + get_pages (msg.response_headers.get_one ("Link")); + }) + .on_error (on_error) + .exec (); + + return GLib.Source.REMOVE; } public static void open_from_id (int64 id){ - var url = "%s/api/v1/accounts/%lld".printf (accounts.formal.instance, id); + var url = "%s/api/v1/accounts/%lld".printf (accounts.active.instance, id); var msg = new Soup.Message ("GET", url); msg.priority = Soup.MessagePriority.HIGH; network.queue (msg, (sess, mess) => { var root = network.parse (mess); - var acc = API.Account.parse (root); + var acc = new API.Account (root); window.open_view (new Views.Profile (acc)); }, (status, reason) => { network.on_error (status, reason); diff --git a/src/Views/Search.vala b/src/Views/Search.vala index 108641d..d96a86c 100644 --- a/src/Views/Search.vala +++ b/src/Views/Search.vala @@ -1,61 +1,64 @@ using Gtk; -public class Tootle.Views.Search : Views.Abstract { +public class Tootle.Views.Search : Views.Base { private string query = ""; - private Entry entry; + private SearchBar bar; + private SearchEntry entry; construct { - view.margin_bottom = 6; + bar = new SearchBar (); + bar.search_mode_enabled = true; + bar.show (); + pack_start (bar, false, false, 0); - entry = new Entry (); - entry.placeholder_text = _("Search"); - entry.secondary_icon_name = "system-search-symbolic"; + entry = new SearchEntry (); entry.width_chars = 25; entry.text = query; - entry.valign = Align.CENTER; entry.show (); - window.header.pack_start (entry); + bar.add (entry); + bar.connect_entry (entry); - destroy.connect (() => entry.destroy ()); entry.activate.connect (() => request ()); entry.icon_press.connect (() => request ()); - } - - public Search () { entry.grab_focus_without_selecting (); + status_button.clicked.connect (request); } private void append_account (API.Account acc) { - var widget = new Widgets.Account (acc); - view.pack_start (widget, false, false, 0); + var status = new API.Status.from_account (acc); + var widget = new Widgets.Status (status); + widget.button_press_event.connect (widget.on_avatar_clicked); + content.pack_start (widget, false, false, 0); + on_content_changed (); } private void append_status (API.Status status) { var widget = new Widgets.Status (status); widget.button_press_event.connect (widget.on_avatar_clicked); - view.pack_start (widget, false, false, 0); + content.pack_start (widget, false, false, 0); + on_content_changed (); } private void append_header (string name) { - var widget = new Label (name); - widget.get_style_context ().add_class ("h4"); + var widget = new Label (@"$name"); widget.halign = Align.START; - widget.margin = 6; - widget.margin_bottom = 0; + widget.margin = 8; + widget.use_markup = true; widget.show (); - view.pack_start (widget, false, false, 0); + content.pack_start (widget, false, false, 0); + on_content_changed (); } private void append_hashtag (string name) { - var text = "#%s".printf (accounts.formal.instance, Soup.URI.encode (name, null), name); - var widget = new Widgets.RichLabel (text); + var encoded = Soup.URI.encode (name, null); + var widget = new Widgets.RichLabel (@"#$name"); widget.use_markup = true; widget.halign = Align.START; widget.margin = 6; widget.margin_bottom = 0; widget.show (); - view.pack_start (widget, false, false, 0); + content.pack_start (widget, false, false, 0); } private void request () { @@ -64,25 +67,31 @@ public class Tootle.Views.Search : Views.Abstract { clear (); return; } - window.reopen_view (this.stack_pos); - var query_encoded = Soup.URI.encode (query, null); - var url = "%s/api/v1/search?q=%s&resolve=true".printf (accounts.formal.instance, query_encoded); - var msg = new Soup.Message("GET", url); - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, mess) => { - var root = network.parse (mess); + new Request.GET ("/api/v2/search") + .with_account (accounts.active) + .with_param ("resolve", "true") + .with_param ("q", Soup.URI.encode (query, null)) + .then ((sess, msg) => { + var root = network.parse (msg); var accounts = root.get_array_member ("accounts"); var statuses = root.get_array_member ("statuses"); var hashtags = root.get_array_member ("hashtags"); clear (); + if (hashtags.get_length () > 0) { + append_header (_("Hashtags")); + hashtags.foreach_element ((array, i, node) => { + append_hashtag (node.get_object ().get_string_member ("name")); + }); + } + if (accounts.get_length () > 0) { append_header (_("Accounts")); accounts.foreach_element ((array, i, node) => { var obj = node.get_object (); - var acc = API.Account.parse (obj); + var acc = new API.Account (obj); append_account (acc); }); } @@ -91,20 +100,13 @@ public class Tootle.Views.Search : Views.Abstract { append_header (_("Statuses")); statuses.foreach_element ((array, i, node) => { var obj = node.get_object (); - var status = API.Status.parse (obj); + var status = new API.Status (obj); append_status (status); }); } - - if (hashtags.get_length () > 0) { - append_header (_("Hashtags")); - hashtags.foreach_element ((array, i, node) => { - append_hashtag (node.get_string ()); - }); - } - - empty_state (); - }); + }) + .on_error (on_error) + .exec (); } } diff --git a/src/Views/Timeline.vala b/src/Views/Timeline.vala index 33b1f7c..fcd5c64 100644 --- a/src/Views/Timeline.vala +++ b/src/Views/Timeline.vala @@ -1,31 +1,25 @@ using Gtk; using Gdk; -public class Tootle.Views.Timeline : Views.Abstract { +public class Tootle.Views.Timeline : Views.Base, IAccountListener, IStreamListener { - protected string timeline; - protected string pars; + public string timeline { get; construct set; } + public bool is_public { get; construct set; default = false; } + + protected InstanceAccount? account = null; protected int limit = 25; protected bool is_last_page = false; protected string? page_next; protected string? page_prev; + protected string? stream; - protected Notificator? notificator; - - public Timeline (string timeline, string pars = "") { - base (); - this.timeline = timeline; - this.pars = pars; - - accounts.switched.connect (on_account_changed); + construct { app.refresh.connect (on_refresh); - destroy.connect (() => { - if (notificator != null) - notificator.close (); - }); - - setup_notificator (); - request (); + status_button.clicked.connect (on_refresh); + connect_account (); + } + ~Timeline () { + streams.unsubscribe (stream, this); } public override string get_icon () { @@ -36,38 +30,32 @@ public class Tootle.Views.Timeline : Views.Abstract { return _("Home"); } - public virtual void on_status_added (API.Status status) { + public override void on_status_added (API.Status status) { prepend (status); } public virtual bool is_status_owned (API.Status status) { - return false; + return status.is_owned (); } public void prepend (API.Status status) { append (status, true); } - public void append (API.Status status, bool first = false){ - if (empty != null) - empty.destroy (); + public void append (API.Status status, bool first = false) { + GLib.Idle.add (() => { + var w = new Widgets.Status (status); + w.button_press_event.connect (w.open); + if (!is_status_owned (status)) + w.avatar.button_press_event.connect (w.on_avatar_clicked); - var separator = new Separator (Orientation.HORIZONTAL); - separator.show (); + content.pack_start (w, false, false, 0); + if (first || status.pinned) + content.reorder_child (w, 0); - var widget = new Widgets.Status (status); - widget.separator = separator; - widget.button_press_event.connect (widget.open); - if (!is_status_owned (status)) - widget.avatar.button_press_event.connect (widget.on_avatar_clicked); - view.pack_start (separator, false, false, 0); - view.pack_start (widget, false, false, 0); - - if (first || status.pinned) { - var new_index = header == null ? 1 : 0; - view.reorder_child (separator, new_index); - view.reorder_child (widget, new_index); - } + on_content_changed (); + return GLib.Source.REMOVE; + }); } public override void clear () { @@ -102,82 +90,51 @@ public class Tootle.Views.Timeline : Views.Abstract { if (page_next != null) return page_next; - var url = "%s/api/v1/timelines/%s?limit=%i".printf (accounts.formal.instance, this.timeline, this.limit); - url += this.pars; - return url; + return @"/api/v1/timelines/$timeline"; } - public virtual void request (){ - if (accounts.current == null) { - empty_state (); - return; - } - - var msg = new Soup.Message ("GET", get_url ()); - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, mess) => { - network.parse_array (mess).foreach_element ((array, i, node) => { - var object = node.get_object (); - if (object != null) { - var status = API.Status.parse (object); - append (status); - } - }); - get_pages (mess.response_headers.get_one ("Link")); - empty_state (); - }, - network.on_error); + public virtual Request append_params (Request req) { + return req.with_param ("limit", limit.to_string ()); } - public virtual void on_refresh (){ + public virtual bool request () { + append_params (new Request.GET (get_url ())) + .with_account (account) + .then_parse_array ((node, msg) => { + var obj = node.get_object (); + if (obj != null) { + var status = new API.Status (obj); + append (status); + } + get_pages (msg.response_headers.get_one ("Link")); + }) + .on_error (on_error) + .exec (); + + return GLib.Source.REMOVE; + } + + public virtual void on_refresh () { + status_button.sensitive = false; clear (); - request (); + status_message = STATUS_LOADING; + GLib.Idle.add (request); } - public virtual Soup.Message? get_stream (){ + public virtual string? get_stream_url () { return null; } - public virtual void on_account_changed (API.Account? account){ - if(account == null) - return; - - var stream = get_stream (); - if (notificator != null && stream != null) { - var old_url = notificator.get_url (); - var new_url = stream.get_uri ().to_string (false); - if (old_url != new_url) { - info ("Updating notificator %s", notificator.get_name ()); - setup_notificator (); - } - } - + public override void on_account_changed (InstanceAccount? acc) { + account = acc; + streams.unsubscribe (stream, this); + streams.subscribe (get_stream_url (), this, out stream); on_refresh (); } - protected void setup_notificator () { - if (notificator != null) - notificator.close (); - - var stream = get_stream (); - if (stream == null) - return; - - notificator = new Notificator (stream); - notificator.status_added.connect ((status) => { - if (can_stream ()) - on_status_added (status); - }); - notificator.start (); - } - - protected virtual bool is_public () { - return false; - } - - protected virtual bool can_stream () { + protected override bool accepts (ref string event) { var allowed_public = true; - if (is_public ()) + if (is_public) allowed_public = settings.live_updates_public; return settings.live_updates && allowed_public; @@ -185,7 +142,7 @@ public class Tootle.Views.Timeline : Views.Abstract { protected override void on_bottom_reached () { if (is_last_page) { - debug ("Last page reached"); + info ("Last page reached"); return; } request (); diff --git a/src/Watchlist.vala b/src/Watchlist.vala deleted file mode 100644 index 603c1e4..0000000 --- a/src/Watchlist.vala +++ /dev/null @@ -1,125 +0,0 @@ -using GLib; -using Gee; - -public class Tootle.Watchlist : Object { - - public ArrayList users = new ArrayList (); - public ArrayList hashtags = new ArrayList (); - public ArrayList notificators = new ArrayList (); - - construct { - accounts.switched.connect (on_account_changed); - } - - public Watchlist () {} - - public virtual void on_account_changed (API.Account? account){ - if (account != null) - reload (); - } - - private void reload () { - info ("Reloading"); - - notificators.@foreach (notificator => { - notificator.close (); - return true; - }); - notificators.clear (); - users.clear (); - hashtags.clear (); - - load (); - info ("Watching for %i users and %i hashtags", users.size, hashtags.size); - } - - private void load () { - var users_array = settings.watched_users.split (","); - foreach (string item in users_array) - add (item, false); - - var hashtags_array = settings.watched_hashtags.split (","); - foreach (string item in hashtags_array) - add (item, true); - } - - public void save () { - var serialized_users = ""; - users.@foreach (item => { - serialized_users += item + ","; - return true; - }); - serialized_users = remove_last_delimiter (serialized_users); - settings.watched_users = serialized_users; - - var serialized_hashtags = ""; - hashtags.@foreach (item => { - serialized_hashtags += item + ","; - return true; - }); - serialized_hashtags = remove_last_delimiter (serialized_hashtags); - settings.watched_hashtags = serialized_hashtags; - - info ("Saved"); - } - - private string remove_last_delimiter (string str) { - var i = str.last_index_of (","); - if (i > -1) - return str.substring (0, i); - else - return str; - } - - private Notificator get_notificator (string hashtag) { - var url = "%s/api/v1/streaming/?stream=hashtag&tag=%s&access_token=%s".printf (accounts.formal.instance, hashtag, accounts.formal.token); - var msg = new Soup.Message ("GET", url); - var notificator = new Notificator (msg); - notificator.status_added.connect (on_status_added); - return notificator; - } - - private void on_status_added (API.Status status) { - var obj = new API.Notification (-1); - obj.type = API.NotificationType.WATCHLIST; - obj.account = status.account; - obj.status = status; - accounts.formal.notification (obj); - } - - public void add (string entity, bool is_hashtag) { - if (entity == "") - return; - - if (is_hashtag) { - hashtags.add (entity); - var notificator = get_notificator (entity); - notificator.start (); - notificators.add (notificator); - info ("Added #%s", entity); - } - else { - users.add (entity); - info ("Added @%s", entity); - } - } - - public void remove (string entity, bool is_hashtag) { - if (entity == "") - return; - - if (is_hashtag) { - var i = hashtags.index_of (entity); - var notificator = notificators.@get(i); - notificator.close (); - notificators.remove_at (i); - hashtags.remove (entity); - info ("Removed #%s", entity); - } - else { - users.remove (entity); - info ("Removed @%s", entity); - } - } - -} diff --git a/src/Widgets/Account.vala b/src/Widgets/Account.vala index 2eeb289..0e7b9c1 100644 --- a/src/Widgets/Account.vala +++ b/src/Widgets/Account.vala @@ -5,15 +5,15 @@ public class Tootle.Widgets.Account : Widgets.Status { public Account (API.Account account) { var status = new API.Status (-1); status.account = account; - status.url = account.url; - status.content = "@%s".printf (account.url, account.acct); - status.created_at = account.created_at; + //status.url = account.url; + //status.content = "@%s".printf (account.url, account.acct); + //status.created_at = account.created_at; base (status); - counters.visible = false; - title_acct.visible = false; - content_label.margin_bottom = 12; + //counters.visible = false; + //title_acct.visible = false; + //content_label.margin_bottom = 12; } protected override bool on_clicked (EventButton ev) { diff --git a/src/Widgets/AccountsButton.vala b/src/Widgets/AccountsButton.vala index f17a207..e48560b 100644 --- a/src/Widgets/AccountsButton.vala +++ b/src/Widgets/AccountsButton.vala @@ -1,168 +1,149 @@ using Gtk; -public class Tootle.Widgets.AccountsButton : MenuButton { +[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/widgets/accounts_button.ui")] +public class Tootle.Widgets.AccountsButton : Gtk.MenuButton, IAccountListener { - const int AVATAR_SIZE = 24; - Granite.Widgets.Avatar avatar; - Grid grid; - Popover menu; - ListBox list; - ModelButton item_settings; - ModelButton item_refresh; - ModelButton item_search; - ModelButton item_favs; - ModelButton item_direct; - ModelButton item_watchlist; + [GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/widgets/accounts_button_item.ui")] + private class Item : Grid { + [GtkChild] + private Widgets.Avatar avatar; + [GtkChild] + private Label name; + [GtkChild] + private Label handle; + [GtkChild] + private Button profile; + [GtkChild] + private Button remove; - private class AccountItemView : ListBoxRow { + public Item (InstanceAccount acc, AccountsButton _self) { + avatar.url = acc.avatar; + name.label = acc.display_name; + handle.label = acc.handle; - private Grid grid; - public Label display_name; - public Label instance; - public Button button; - public int id = -1; + profile.clicked.connect (() => { + Views.Profile.open_from_id (acc.id); + _self.active = false; + }); - construct { - can_default = false; - - grid = new Grid (); - grid.margin = 6; - grid.margin_start = 14; - - display_name = new Label (""); - display_name.hexpand = true; - display_name.halign = Align.START; - display_name.use_markup = true; - instance = new Label (""); - instance.halign = Align.START; - button = new Button.from_icon_name ("window-close-symbolic", IconSize.SMALL_TOOLBAR); - button.receives_default = false; - button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - - grid.attach (display_name, 1, 0, 1, 1); - grid.attach (instance, 1, 1, 1, 1); - grid.attach (button, 2, 0, 2, 2); - add (grid); - show_all (); + remove.clicked.connect (() => { + _self.active = false; + accounts.remove (acc); + }); } - public AccountItemView (){ - button.clicked.connect (() => accounts.remove (id)); + public Item.add_new () { + name.label = _("New Account"); + handle.label = _("Click to add"); + profile.destroy (); + remove.destroy (); } - } - construct{ - avatar = new Granite.Widgets.Avatar.with_default_icon (AVATAR_SIZE); - list = new ListBox (); + private bool invalidated = true; - var item_separator = new Separator (Orientation.HORIZONTAL); - item_separator.hexpand = true; + [GtkChild] + private Widgets.Avatar avatar; + [GtkChild] + private Spinner spinner; + + [GtkChild] + private ListBox account_list; + + [GtkChild] + private ModelButton item_accounts; + [GtkChild] + private ModelButton item_prefs; + [GtkChild] + private ModelButton item_refresh; + [GtkChild] + private ModelButton item_search; + [GtkChild] + private ModelButton item_favs; + [GtkChild] + private ModelButton item_direct; + [GtkChild] + private ModelButton item_watchlist; + + construct { + connect_account (); - item_refresh = new ModelButton (); - item_refresh.text = _("Refresh"); item_refresh.clicked.connect (() => app.refresh ()); Desktop.set_hotkey_tooltip (item_refresh, null, app.ACCEL_REFRESH); - item_favs = new ModelButton (); - item_favs.text = _("Favorites"); item_favs.clicked.connect (() => window.open_view (new Views.Favorites ())); - - item_direct = new ModelButton (); - item_direct.text = _("Direct Messages"); item_direct.clicked.connect (() => window.open_view (new Views.Direct ())); - - item_search = new ModelButton (); - item_search.text = _("Search"); item_search.clicked.connect (() => window.open_view (new Views.Search ())); + //item_watchlist.clicked.connect (() => Dialogs.WatchlistEditor.open ()); + item_prefs.clicked.connect (() => Dialogs.Preferences.open ()); - item_watchlist = new ModelButton (); - item_watchlist.text = _("Watchlist"); - item_watchlist.clicked.connect (() => Dialogs.WatchlistEditor.open ()); + // network.started.connect (() => spinner.show ()); + // network.finished.connect (() => spinner.hide ()); - item_settings = new ModelButton (); - item_settings.text = _("Settings"); - item_settings.clicked.connect (() => Dialogs.Preferences.open ()); + on_account_changed (null); - grid = new Grid (); - grid.orientation = Orientation.VERTICAL; - grid.width_request = 200; - grid.attach (list, 0, 1, 1, 1); - grid.attach (item_separator, 0, 3, 1, 1); - grid.attach (item_favs, 0, 4, 1, 1); - grid.attach (item_direct, 0, 5, 1, 1); - grid.attach (new Separator (Orientation.HORIZONTAL), 0, 6, 1, 1); - grid.attach (item_refresh, 0, 7, 1, 1); - grid.attach (item_search, 0, 8, 1, 1); - grid.attach (item_watchlist, 0, 9, 1, 1); - grid.attach (item_settings, 0, 10, 1, 1); - grid.show_all (); - - menu = new Popover (null); - menu.add (grid); - - get_style_context ().add_class ("button_avatar"); - popover = menu; - add (avatar); - show_all (); - - accounts.updated.connect (accounts_updated); - accounts.switched.connect (account_switched); - list.row_activated.connect (row => { - var widget = row as AccountItemView; - if (widget.id == -1) { - Dialogs.NewAccount.open (); - return; - } - if (widget.id == settings.current_account) - Views.Profile.open_from_id (accounts.current.id); - else - accounts.switch_account (widget.id); - - menu.popdown (); - }); - } - - private void accounts_updated (GenericArray accounts) { - list.forall (widget => widget.destroy ()); - int i = -1; - accounts.foreach (account => { - i++; - var widget = new AccountItemView (); - widget.id = i; - widget.display_name.label = "@"+account.username+""; - widget.instance.label = account.get_pretty_instance (); - list.add (widget); + notify["active"].connect (() => { + if (active && invalidated) + rebuild (); }); - var add_account = new AccountItemView (); - add_account.display_name.label = _("New Account"); - add_account.instance.label = _("Click to add"); - add_account.button.hide (); - list.add (add_account); - update_selection (); + account_list.row_activated.connect (on_selection_changed) ; } - private void account_switched (API.Account? account) { - if (account == null) - avatar.show_default (AVATAR_SIZE); - else - network.load_avatar (account.avatar, avatar, get_avatar_size ()); + protected void on_selection_changed (ListBoxRow r) { + var i = r.get_index (); + if (i >= accounts.saved.size) { + active = false; + window.open_view (new Views.NewAccount (true)); + return; + } + + var account = accounts.saved.@get (i); + if (accounts.active == account) + return; + + accounts.switch_account (i); } - private void update_selection () { - var id = settings.current_account; - var row = list.get_row_at_index (id); - if (row != null) - list.select_row (row); + public virtual void on_accounts_changed (Gee.ArrayList accounts) { + invalidated = true; + if (active) + rebuild (); } - public int get_avatar_size () { - return AVATAR_SIZE * get_style_context ().get_scale (); + public virtual void on_account_changed (InstanceAccount? account) { + if (account == null) { + avatar.url = null; + item_accounts.text = "" + _("No active account") + ""; + } + else { + avatar.url = account.avatar; + item_accounts.text = @"$(account.display_name)\n$(account.handle) "; + } + item_accounts.use_markup = true; } - public AccountsButton () { - account_switched (accounts.current); + private void rebuild () { + account_list.@foreach (w => account_list.remove (w)); + accounts.saved.@foreach (acc => { + var item = new Item (acc, this); + var row = new ListBoxRow (); + row.add (item); + row.show (); + + account_list.insert (row, -1); + if (accounts.active == acc) + row.activate (); + + return true; + }); + var new_row = new ListBoxRow (); + new_row.add (new Item.add_new ()); + new_row.selectable = false; + new_row.show (); + account_list.insert (new_row, -1); + + invalidated = false; } } diff --git a/src/Widgets/Attachment/Box.vala b/src/Widgets/Attachment/Box.vala new file mode 100644 index 0000000..281e14e --- /dev/null +++ b/src/Widgets/Attachment/Box.vala @@ -0,0 +1,69 @@ +using Gtk; +using GLib; +using Gee; + +public class Tootle.Widgets.Attachment.Box : FlowBox { + + public bool editing { get; construct set; } + + construct { + hexpand = true; + can_focus = false; + selection_mode = SelectionMode.NONE; + } + + public Box (bool editing = false) { + Object (editing: editing); + } + + public void select () { + var filter = new Gtk.FileFilter (); + filter.add_mime_type ("image/jpeg"); + filter.add_mime_type ("image/png"); + filter.add_mime_type ("image/gif"); + filter.add_mime_type ("video/webm"); + filter.add_mime_type ("video/mp4"); + + var chooser = new Gtk.FileChooserDialog ( + _("Select media files to add"), + null, + Gtk.FileChooserAction.OPEN, + _("_Cancel"), + Gtk.ResponseType.CANCEL, + _("_Open"), + Gtk.ResponseType.ACCEPT); + + chooser.select_multiple = true; + chooser.set_filter (filter); + + if (chooser.run () == ResponseType.ACCEPT) { + show (); + foreach (unowned string uri in chooser.get_uris ()) { + //var widget = new ImageAttachment.upload (uri); + //append_widget (widget); + } + } + chooser.close (); + } + + public bool populate (ArrayList? list) { + if (list == null) + return false; + + var max = 6; + if (list.size % 2 == 0) + max = 2; + + //max_children_per_line = (int)Math.fmin (list.size, 5); + max_children_per_line = max; + list.@foreach (obj => pack (obj)); + return true; + } + + public bool pack (API.Attachment obj) { + var w = new Widgets.Attachment.Item (obj); + insert (w, -1); + return true; + } + +} diff --git a/src/Widgets/Attachment/Item.vala b/src/Widgets/Attachment/Item.vala new file mode 100644 index 0000000..9260715 --- /dev/null +++ b/src/Widgets/Attachment/Item.vala @@ -0,0 +1,105 @@ +using Gtk; +using Gdk; + +public class Tootle.Widgets.Attachment.Item : EventBox { + + public API.Attachment attachment { get; construct set; } + + private Cache.Reference? cached; + + public Item (API.Attachment obj) { + Object (attachment: obj); + } + ~Item () { + cache.unload (cached); + } + + construct { + get_style_context ().add_class ("attachment"); + width_request = height_request = 128; + hexpand = true; + tooltip_text = attachment.description ?? _("No description is available"); + + button_press_event.connect (on_clicked); + + show (); + on_request (); + } + + protected void on_request () { + cached = null; + on_redraw (); + cache.load (attachment.preview_url, on_cache_result); + } + + protected void on_redraw () { + var w = get_allocated_width (); + var h = get_allocated_height (); + queue_draw_area (0, 0, w, h); + } + + protected void on_cache_result (Cache.Reference? result) { + cached = result; + on_redraw (); + } + + protected void download () { + Desktop.download (attachment.url, path => { + app.toast (_("Attachment downloaded")); + }); + } + protected void open () { + Desktop.download (attachment.url, path => { + Desktop.open_uri (path); + }); + } + + public override bool draw (Cairo.Context ctx) { + base.draw (ctx); + var w = get_allocated_width (); + var h = get_allocated_height (); + var style = get_style_context (); + var border_radius = style.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS, style.get_state ()).get_int (); + + if (cached != null) { + if (cached.loading) { + Drawing.center (ctx, w, h, 32, 32); + get_style_context ().render_activity (ctx, 0, 0, 32, 32); + } + else { + var thumb = Drawing.make_thumbnail (cached.data, w, h); + Drawing.draw_rounded_rect (ctx, 0, 0, w, h, border_radius); + Drawing.center (ctx, w, h, thumb.width, thumb.height); + Gdk.cairo_set_source_pixbuf (ctx, thumb, 0, 0); + ctx.fill (); + } + } + + return Gdk.EVENT_STOP; + } + + protected virtual bool on_clicked (EventButton ev) { + if (ev.button == 1) { + open (); + return true; + } + else if (ev.button == 3) { + var menu = new Gtk.Menu (); + + var item_open = new Gtk.MenuItem.with_label (_("Open")); + item_open.activate.connect (open); + menu.add (item_open); + + var item_download = new Gtk.MenuItem.with_label (_("Download")); + item_download.activate.connect (download); + menu.add (item_download); + + menu.show_all (); + menu.attach_widget = this; + menu.popup_at_pointer (); + return true; + } + return false; + } + +} diff --git a/src/Widgets/AttachmentGrid.vala b/src/Widgets/AttachmentGrid.vala deleted file mode 100644 index c3f73fd..0000000 --- a/src/Widgets/AttachmentGrid.vala +++ /dev/null @@ -1,91 +0,0 @@ -using Gtk; -using GLib; - -public class Tootle.Widgets.AttachmentGrid : Grid { - - private int counter = 0; - private bool allow_editing; - - construct { - hexpand = true; - } - - public AttachmentGrid (bool edit = false) { - allow_editing = edit; - } - - public void append (API.Attachment attachment) { - var widget = new ImageAttachment (attachment); - attach_widget (widget); - } - public void append_widget (ImageAttachment widget) { - attach_widget (widget); - } - - private void attach_widget (ImageAttachment widget) { - attach (widget, counter++, 1); - column_spacing = row_spacing = 12; - show_all (); - } - - public void pack (API.Attachment[] attachments) { - clear (); - var len = attachments.length; - - if (len == 1) { - var widget = new ImageAttachment (attachments[0]); - attach_widget (widget); - widget.fill_parent (); - } - else { - foreach (API.Attachment attachment in attachments) { - append (attachment); - } - } - } - - private void clear () { - forall (widget => widget.destroy ()); - } - - public void select () { - var filter = new Gtk.FileFilter (); - filter.add_mime_type ("image/jpeg"); - filter.add_mime_type ("image/png"); - filter.add_mime_type ("image/gif"); - filter.add_mime_type ("video/webm"); - filter.add_mime_type ("video/mp4"); - - var chooser = new Gtk.FileChooserDialog ( - _("Select media files to add"), - null, - Gtk.FileChooserAction.OPEN, - _("_Cancel"), - Gtk.ResponseType.CANCEL, - _("_Open"), - Gtk.ResponseType.ACCEPT); - - chooser.select_multiple = true; - chooser.set_filter (filter); - - if (chooser.run () == Gtk.ResponseType.ACCEPT) { - show (); - foreach (unowned string uri in chooser.get_uris ()) { - var widget = new ImageAttachment.upload (uri); - append_widget (widget); - } - } - chooser.close (); - } - - public string get_uri_array () { - var str = ""; - get_children ().@foreach (w => { - var widget = (ImageAttachment) w; - if (widget.attachment != null) - str += "&media_ids[]=%lld".printf (widget.attachment.id); - }); - return str; - } - -} diff --git a/src/Widgets/Avatar.vala b/src/Widgets/Avatar.vala new file mode 100644 index 0000000..0da9d46 --- /dev/null +++ b/src/Widgets/Avatar.vala @@ -0,0 +1,71 @@ +using Gtk; +using Gdk; + +public class Tootle.Widgets.Avatar : EventBox { + + public string? url { get; set; } + public int size { get; set; default = 48; } + + private Cache.Reference? cached; + + construct { + get_style_context ().add_class ("avatar"); + notify["url"].connect (on_url_updated); + notify["size"].connect (on_redraw); + Screen.get_default ().monitors_changed.connect (on_redraw); + on_url_updated (); + } + + public Avatar (int size = this.size) { + Object (size: size); + on_redraw (); + } + + ~Avatar () { + notify["url"].disconnect (on_url_updated); + Screen.get_default ().monitors_changed.disconnect (on_redraw); + cache.unload (cached); + } + + private void on_url_updated () { + cached = null; + on_redraw (); + cache.load (url, on_cache_result); + } + + private void on_cache_result (Cache.Reference? result) { + cached = result; + on_redraw (); + } + + public int get_scaled_size () { + return size * get_scale_factor (); + } + + private void on_redraw () { + set_size_request (get_scaled_size (), get_scaled_size ()); + queue_draw_area (0, 0, size, size); + } + + public override bool draw (Cairo.Context ctx) { + var w = get_allocated_width (); + var h = get_allocated_height (); + var style = get_style_context (); + var border_radius = style.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS, style.get_state ()).get_int (); + Pixbuf pixbuf; + + Drawing.draw_rounded_rect (ctx, 0, 0, w, h, border_radius); + if (cached != null && !cached.loading) { + pixbuf = cached.data.scale_simple (get_scaled_size (), get_scaled_size (), InterpType.BILINEAR); + } + else { + pixbuf = IconTheme.get_default () + .load_icon_for_scale ("avatar-default", size, get_scale_factor (), IconLookupFlags.GENERIC_FALLBACK); + } + Gdk.cairo_set_source_pixbuf (ctx, pixbuf, 0, 0); + ctx.fill (); + + return Gdk.EVENT_STOP; + } + +} diff --git a/src/Widgets/ImageAttachment.vala b/src/Widgets/ImageAttachment.vala deleted file mode 100644 index ba77c17..0000000 --- a/src/Widgets/ImageAttachment.vala +++ /dev/null @@ -1,187 +0,0 @@ -using Gtk; -using Gdk; - -public class Tootle.Widgets.ImageAttachment : DrawingArea { - - public API.Attachment? attachment; - private bool editable = false; - private bool fill = false; - - private Pixbuf? pixbuf = null; - private static Pixbuf? pixbuf_error; - private int center_x = 0; - private int center_y = 0; - - private Soup.Message? image_request; - - construct { - if (pixbuf_error == null) - pixbuf_error = IconTheme.get_default ().load_icon ("image-missing", 32, IconLookupFlags.GENERIC_FALLBACK); - - hexpand = true; - vexpand = true; - add_events (EventMask.BUTTON_PRESS_MASK); - draw.connect (on_draw); - button_press_event.connect (on_clicked); - } - - ~ImageAttachment () { - network.cancel_request (image_request); - } - - public ImageAttachment (API.Attachment obj) { - attachment = obj; - image_request = network.load_pixbuf (attachment.preview_url, on_ready); - set_size_request (32, 128); - show_all (); - } - - public ImageAttachment.upload (string uri) { - halign = Align.START; - valign = Align.START; - set_size_request (100, 100); - show_all (); - try { - GLib.File file = File.new_for_uri (uri); - uint8[] contents; - file.load_contents (null, out contents, null); - var type = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, 0); - var mime = type.get_content_type (); - - info ("Uploading %s (%s)", uri, mime); - show (); - - var buffer = new Soup.Buffer.take (contents); - var multipart = new Soup.Multipart (Soup.FORM_MIME_TYPE_MULTIPART); - multipart.append_form_file ("file", mime.replace ("/", "."), mime, buffer); - var url = "%s/api/v1/media".printf (accounts.formal.instance); - var msg = Soup.Form.request_new_from_multipart (url, multipart); - - network.queue (msg, (sess, mess) => { - var root = network.parse (mess); - attachment = API.Attachment.parse (root); - editable = true; - invalidate (); - network.load_pixbuf (attachment.preview_url, on_ready); - info ("Uploaded media: %lld", attachment.id); - }); - } - catch (Error e) { - app.error (_("File read error"), _("Can't read file %s: %s").printf (uri, e.message)); - warning (e.message); - } - } - - private void on_ready (Pixbuf? result) { - if (result == null) - result = pixbuf_error; - - pixbuf = result; - invalidate (); - } - - private void invalidate () { - var w = get_allocated_width (); - var h = get_allocated_height (); - if (fill) { - var h_scaled = (pixbuf.height * w) / pixbuf.width; - if (h_scaled > pixbuf.height) { - halign = Align.START; - set_size_request (pixbuf.width, pixbuf.height); - } - else { - halign = Align.FILL; - set_size_request (1, h_scaled); - } - } - queue_draw_area (0, 0, w, h); - } - - private void calc_center (int w, int h, int size_w, int size_h, Cairo.Context? ctx = null) { - center_x = w/2 - size_w/2; - center_y = h/2 - size_h/2; - - if (ctx != null) - ctx.translate (center_x, center_y); - } - - public void fill_parent () { - fill = true; - size_allocate.connect (on_size_changed); - on_size_changed (); - } - - public void on_size_changed () { - if (fill && pixbuf != null) - invalidate (); - } - - private bool on_draw (Widget widget, Cairo.Context ctx) { - var w = widget.get_allocated_width (); - var h = widget.get_allocated_height (); - if (halign == Align.START) { - w = pixbuf.width; - h = pixbuf.height; - } - - //Draw frame - ctx.set_source_rgba (1, 1, 1, 1); - Drawing.draw_rounded_rect (ctx, 0, 0, w, h, 4); - ctx.fill (); - - //Draw image, spinner or an error icon - if (pixbuf != null) { - var thumbnail = Drawing.make_pixbuf_thumbnail (pixbuf, w, h, fill); - Drawing.draw_rounded_rect (ctx, 0, 0, w, h, 4); - calc_center (w, h, thumbnail.width, thumbnail.height, ctx); - Gdk.cairo_set_source_pixbuf (ctx, thumbnail, 0, 0); - ctx.fill (); - } - else { - calc_center (w, h, 32, 32, ctx); - set_state_flags (StateFlags.CHECKED, false); //Y U NO SPIN - get_style_context ().render_activity (ctx, 0, 0, 32, 32); - } - - return false; - } - - private bool on_clicked (EventButton ev){ - switch (ev.button) { - case 3: - return open_menu (ev.button, ev.time); - case 1: - return Desktop.open_uri (attachment.url); - } - return false; - } - - public virtual bool open_menu (uint button, uint32 time) { - var menu = new Gtk.Menu (); - - if (editable && attachment != null) { - var item_remove = new Gtk.MenuItem.with_label (_("Remove")); - item_remove.activate.connect (() => destroy ()); - menu.add (item_remove); - menu.add (new Gtk.SeparatorMenuItem ()); - } - - var item_open_link = new Gtk.MenuItem.with_label (_("Open in Browser")); - item_open_link.activate.connect (() => Desktop.open_uri (attachment.url)); - var item_copy_link = new Gtk.MenuItem.with_label (_("Copy Link")); - item_copy_link.activate.connect (() => Desktop.copy (attachment.url)); - var item_download = new Gtk.MenuItem.with_label (_("Download")); - item_download.activate.connect (() => Desktop.download_file (attachment.url)); - menu.add (item_open_link); - if (attachment.type != "unknown") - menu.add (item_download); - menu.add (new Gtk.SeparatorMenuItem ()); - menu.add (item_copy_link); - - menu.show_all (); - menu.attach_widget = this; - menu.popup_at_pointer (); - return true; - } - -} diff --git a/src/Widgets/ImageToggleButton.vala b/src/Widgets/ImageToggleButton.vala deleted file mode 100644 index 7bdd4b8..0000000 --- a/src/Widgets/ImageToggleButton.vala +++ /dev/null @@ -1,22 +0,0 @@ -using Gtk; - -public class Tootle.Widgets.ImageToggleButton : ToggleButton { - - public Image icon; - public IconSize size; - - public ImageToggleButton (string icon_name, IconSize icon_size = IconSize.BUTTON) { - valign = Align.CENTER; - size = icon_size; - icon = new Image.from_icon_name (icon_name, icon_size); - add (icon); - show_all (); - } - - public void set_action () { - can_default = false; - set_focus_on_click (false); - get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - } - -} diff --git a/src/Widgets/Notification.vala b/src/Widgets/Notification.vala index 099e3f0..0984d25 100644 --- a/src/Widgets/Notification.vala +++ b/src/Widgets/Notification.vala @@ -1,92 +1,34 @@ using Gtk; using Granite; -public class Tootle.Widgets.Notification : Grid { +public class Tootle.Widgets.Notification : Widgets.Status { - private API.Notification notification; + public API.Notification notification { get; construct set; } - public Separator? separator; - private Image image; - private Widgets.RichLabel label; - private Widgets.Status? status_widget; - private Button dismiss; + public Notification (API.Notification obj) { + API.Status status; + if (obj.status != null) + status = obj.status; + else + status = new API.Status.from_account (obj.account); - construct { - margin = 6; + Object (notification: obj, status: status); + this.kind = obj.kind; + } - image = new Image.from_icon_name ("notification-symbolic", IconSize.BUTTON); - image.margin_start = 32; - image.margin_end = 6; - label = new RichLabel (_("Unknown Notification")); - label.hexpand = true; - label.halign = Align.START; - dismiss = new Button.from_icon_name ("window-close-symbolic", IconSize.BUTTON); - dismiss.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - dismiss.tooltip_text = _("Dismiss"); - dismiss.clicked.connect (() => { + protected override void on_kind_changed () { + if (kind == null) + return; + + header_icon.visible = header_label.visible = true; + header_icon.icon_name = kind.get_icon (); + header_label.label = kind.get_desc (notification.account); + } + + protected override void on_status_removed (int64 id) { + if (id == notification.status.id) notification.dismiss (); - destroy (); - }); - - attach (image, 1, 2); - attach (label, 2, 2); - attach (dismiss, 3, 2); - show_all (); - } - - public Notification (API.Notification _notification) { - notification = _notification; - image.icon_name = notification.type.get_icon (); - label.set_label (notification.type.get_desc (notification.account)); - get_style_context ().add_class ("notification"); - - if (notification.status != null) - network.status_removed.connect (on_status_removed); - - destroy.connect (() => { - if (separator != null) - separator.destroy (); - separator = null; - status_widget = null; - }); - - if (notification.status != null){ - status_widget = new Widgets.Status (notification.status, true); - status_widget.is_notification = true; - status_widget.button_press_event.connect (status_widget.open); - status_widget.avatar.button_press_event.connect (status_widget.on_avatar_clicked); - attach (status_widget, 1, 3, 3, 1); - } - - if (notification.type == API.NotificationType.FOLLOW_REQUEST) { - var box = new Box (Orientation.HORIZONTAL, 6); - box.margin_start = 32 + 16 + 8; - var accept = new Button.with_label (_("Accept")); - box.pack_start (accept, false, false, 0); - var reject = new Button.with_label (_("Reject")); - box.pack_start (reject, false, false, 0); - - attach (box, 1, 3, 3, 1); - box.show_all (); - - accept.clicked.connect (() => { - destroy (); - notification.accept_follow_request (); - }); - reject.clicked.connect (() => { - destroy (); - notification.reject_follow_request (); - }); - } - } - - private void on_status_removed (int64 id) { - if (id == notification.status.id) { - if (notification.type == API.NotificationType.WATCHLIST) - notification.dismiss (); - - destroy (); - } - } + base.on_status_removed (id); + } } diff --git a/src/Widgets/RichLabel.vala b/src/Widgets/RichLabel.vala index 9d7fba4..f4c6d39 100644 --- a/src/Widgets/RichLabel.vala +++ b/src/Widgets/RichLabel.vala @@ -1,13 +1,22 @@ using Gtk; +using Gee; public class Tootle.Widgets.RichLabel : Label { - public weak API.Mention[]? mentions; + public weak ArrayList? mentions; + + construct { + use_markup = true; + xalign = 0; + wrap_mode = Pango.WrapMode.WORD_CHAR; + justify = Justification.LEFT; + single_line_mode = false; + set_line_wrap (true); + activate_link.connect (open_link); + } public RichLabel (string text) { set_label (text); - set_use_markup (true); - activate_link.connect (open_link); } public static string escape_entities (string content) { @@ -29,23 +38,16 @@ public class Tootle.Widgets.RichLabel : Label { base.set_markup (Html.simplify(escape_entities (text))); } - public void wrap_words () { - halign = Align.START; - single_line_mode = false; - set_line_wrap (true); - wrap_mode = Pango.WrapMode.WORD_CHAR; - justify = Justification.LEFT; - xalign = 0; - } - public bool open_link (string url) { + if ("tootle://" in url) + return false; + if (mentions != null){ - foreach (API.Mention mention in mentions) { - if (url == mention.url){ + mentions.@foreach (mention => { + if (url == mention.url) Views.Profile.open_from_id (mention.id); - return true; - } - } + return true; + }); } if ("/tags/" in url) { @@ -56,39 +58,37 @@ public class Tootle.Widgets.RichLabel : Label { } if ("@" in url || "tags" in url) { - var query = Soup.URI.encode (url, null); - var msg_url = "%s/api/v1/search?q=%s&resolve=true".printf (accounts.formal.instance, query); - var msg = new Soup.Message("GET", msg_url); - msg.priority = Soup.MessagePriority.HIGH; - network.inject (msg, Network.INJECT_TOKEN); - network.queue (msg, (sess, mess) => { - var root = network.parse (mess); - var accounts = root.get_array_member ("accounts"); - var statuses = root.get_array_member ("statuses"); - var hashtags = root.get_array_member ("hashtags"); + new Request.GET ("/api/v2/search") + .with_account (accounts.active) + .with_param ("resolve", "true") + .with_param ("q", Soup.URI.encode (url, null)) + .then ((sess, mess) => { + var root = network.parse (mess); + var accounts = root.get_array_member ("accounts"); + var statuses = root.get_array_member ("statuses"); + var hashtags = root.get_array_member ("hashtags"); - if (accounts.get_length () > 0) { - var item = accounts.get_object_element (0); - var obj = API.Account.parse (item); - window.open_view (new Views.Profile (obj)); - } - else if (statuses.get_length () > 0) { - var item = accounts.get_object_element (0); - var obj = API.Status.parse (item); - window.open_view (new Views.ExpandedStatus (obj)); - } - else if (hashtags.get_length () > 0) { - var item = accounts.get_object_element (0); - var obj = API.Tag.parse (item); - window.open_view (new Views.Hashtag (obj.name)); - } - else { - Desktop.open_uri (url); - } - - }, (status, reason) => { - open_link_fallback (url, reason); - }); + if (accounts.get_length () > 0) { + var item = accounts.get_object_element (0); + var obj = new API.Account (item); + window.open_view (new Views.Profile (obj)); + } + else if (statuses.get_length () > 0) { + var item = accounts.get_object_element (0); + var obj = new API.Status (item); + window.open_view (new Views.ExpandedStatus (obj)); + } + else if (hashtags.get_length () > 0) { + var item = accounts.get_object_element (0); + var obj = new API.Tag (item); + window.open_view (new Views.Hashtag (obj.name)); + } + else { + Desktop.open_uri (url); + } + }) + .on_error ((status, reason) => open_link_fallback (url, reason)) + .exec (); } else { Desktop.open_uri (url); @@ -97,18 +97,9 @@ public class Tootle.Widgets.RichLabel : Label { } public bool open_link_fallback (string url, string reason) { - warning ("Can't resolve url: " + url); - warning ("Reason: " + reason); - - var toast = window.toast; - toast.title = reason; - toast.set_default_action (_("Open in Browser")); - ulong signal_id = 0; - signal_id = toast.default_action.connect (() => { - Desktop.open_uri (url); - toast.disconnect (signal_id); - }); - toast.send_notification (); + warning (@"Can't resolve url: $url"); + warning (@"Reason: $reason"); + Desktop.open_uri (url); return true; } diff --git a/src/Widgets/Status.vala b/src/Widgets/Status.vala index 361df85..e1e02de 100644 --- a/src/Widgets/Status.vala +++ b/src/Widgets/Status.vala @@ -1,251 +1,191 @@ using Gtk; using Gdk; -using Granite; +[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/widgets/status.ui")] public class Tootle.Widgets.Status : EventBox { - public API.Status status; - public bool is_notification = false; - public const int AVATAR_SIZE = 32; + public API.Status status { get; construct set; } + public API.NotificationType? kind { get; construct set; } - public Separator? separator; - public Granite.Widgets.Avatar avatar; + [GtkChild] + protected Separator separator; + [GtkChild] protected Grid grid; - protected Widgets.RichLabel title_user; - protected Label title_date; - protected Label title_acct; - public Revealer revealer; - protected Widgets.RichLabel content_label; - protected Widgets.RichLabel? content_spoiler; - protected Button? spoiler_button; - protected Box title_box; - protected Widgets.AttachmentGrid attachments; - protected Image pin_indicator; - protected Box counters; - protected Label replies; - protected Label reblogs; - protected Label favorites; - protected Widgets.ImageToggleButton reblog; - protected Widgets.ImageToggleButton favorite; - protected Widgets.ImageToggleButton reply; + [GtkChild] + protected Image header_icon; + [GtkChild] + protected Widgets.RichLabel header_label; + + [GtkChild] + public Widgets.Avatar avatar; + [GtkChild] + protected Widgets.RichLabel handle_label; + [GtkChild] + protected Label date_label; + [GtkChild] + protected Image pin_indicator; + [GtkChild] + public Revealer revealer; + [GtkChild] + protected Widgets.RichLabel content; + [GtkChild] + protected Widgets.RichLabel revealer_content; + [GtkChild] + protected Widgets.Attachment.Box attachments; + + [GtkChild] + protected Box actions; + [GtkChild] + protected Button reply_button; + [GtkChild] + protected ToggleButton reblog_button; + [GtkChild] + protected Image reblog_icon; + [GtkChild] + protected ToggleButton favorite_button; + + protected string escaped_spoiler { + owned get { + if (status.formal.has_spoiler) { + var text = Html.simplify (status.formal.spoiler_text ?? ""); + var label = _("[ Toggle content ]"); + text += @" $label"; + return text; + } + else + return Html.simplify (status.formal.content); + } + } + + protected string escaped_content { + owned get { + return status.formal.has_spoiler ? Html.simplify (status.formal.content) : ""; + } + } + + protected string handle { + owned get { + var name = Html.simplify (status.formal.account.display_name); + var handle = Html.simplify (status.formal.account.acct); + return @"$name @$handle"; + } + } + + protected string date { + owned get { + var timeval = GLib.TimeVal (); + GLib.DateTime? date = null; + if (timeval.from_iso8601 (status.formal.created_at)) + date = new GLib.DateTime.from_timeval_local (timeval); + + return Granite.DateTime.get_relative_datetime (date); + } + } construct { - grid = new Grid (); - - avatar = new Granite.Widgets.Avatar.with_default_icon (AVATAR_SIZE); - avatar.valign = Align.START; - avatar.margin_top = 6; - avatar.margin_start = 6; - avatar.margin_end = 6; - - title_box = new Box (Orientation.HORIZONTAL, 6); - title_box.hexpand = true; - title_box.margin_end = 12; - title_box.margin_top = 6; - - title_user = new Widgets.RichLabel (""); - title_box.pack_start (title_user, false, false, 0); - - title_acct = new Label (""); - title_acct.opacity = 0.5; - title_acct.ellipsize = Pango.EllipsizeMode.END; - title_box.pack_start (title_acct, false, false, 0); - - title_date = new Label (""); - title_date.opacity = 0.5; - title_date.ellipsize = Pango.EllipsizeMode.END; - title_box.pack_end (title_date, false, false, 0); - title_box.show_all (); - - pin_indicator = new Image.from_icon_name ("view-pin-symbolic", IconSize.MENU); - pin_indicator.opacity = 0.5; - title_box.pack_end (pin_indicator, false, false, 0); - - content_label = new Widgets.RichLabel (""); - content_label.wrap_words (); - - attachments = new Widgets.AttachmentGrid (); - - var revealer_box = new Box (Orientation.VERTICAL, 6); - revealer_box.margin_end = 12; - revealer_box.add (content_label); - revealer_box.add (attachments); - revealer = new Revealer (); - revealer.reveal_child = true; - revealer.add (revealer_box); - - reblogs = new Label ("0"); - favorites = new Label ("0"); - replies = new Label ("0"); - - reblog = new Widgets.ImageToggleButton ("media-playlist-repeat-symbolic"); - reblog.set_action (); - reblog.tooltip_text = _("Boost"); - reblog.toggled.connect (() => { - if (reblog.sensitive) - status.get_formal ().set_reblogged (reblog.get_active ()); - }); - favorite = new Widgets.ImageToggleButton ("emblem-favorite-symbolic"); - favorite.set_action (); - favorite.tooltip_text = _("Favorite"); - favorite.toggled.connect (() => { - if (favorite.sensitive) - status.get_formal ().set_favorited (favorite.get_active ()); - }); - reply = new Widgets.ImageToggleButton ("mail-replied-symbolic"); - reply.set_action (); - reply.tooltip_text = _("Reply"); - reply.toggled.connect (() => { - reply.set_active (false); - Dialogs.Compose.reply (status.get_formal ()); - }); - - counters = new Box (Orientation.HORIZONTAL, 6); - counters.margin_top = 6; - counters.margin_bottom = 6; - counters.add (reblog); - counters.add (reblogs); - counters.add (favorite); - counters.add (favorites); - counters.add (reply); - counters.add (replies); - counters.show_all (); - - add (grid); - grid.attach (avatar, 1, 1, 1, 4); - grid.attach (title_box, 2, 2, 1, 1); - grid.attach (revealer, 2, 4, 1, 1); - grid.attach (counters, 2, 5, 1, 1); - show_all (); - button_press_event.connect (on_clicked); - } + streams.status_removed.connect (on_status_removed); + content.activate_link.connect (on_toggle_spoiler); + notify["kind"].connect (on_kind_changed); - public Status (API.Status status, bool notification = false) { - this.status = status; - this.status.updated.connect (rebind); - is_notification = notification; - - if (status.reblog != null) { - var image = new Image.from_icon_name("media-playlist-repeat-symbolic", IconSize.BUTTON); - image.halign = Align.END; - image.margin_end = 6; - image.margin_top = 6; - image.show (); - - var label_text = API.NotificationType.REBLOG_REMOTE_USER.get_desc (status.account); - var label = new Widgets.RichLabel (label_text); - label.halign = Align.START; - label.margin_top = 6; - label.show (); - - grid.attach (image, 1, 0, 1, 1); - grid.attach (label, 2, 0, 2, 1); + if (kind == null) { + if (status.reblog != null) + kind = API.NotificationType.REBLOG_REMOTE_USER; } - if (status.has_spoiler ()) { - revealer.reveal_child = false; - var spoiler_box = new Box (Orientation.HORIZONTAL, 6); - spoiler_box.margin_end = 12; + status.formal.bind_property ("favorited", favorite_button, "active", BindingFlags.SYNC_CREATE); + favorite_button.clicked.connect (() => { + status.action (status.formal.favorited ? "unfavourite" : "favourite"); + }); - var spoiler_button_text = _("Toggle content"); - if (status.sensitive && status.attachments != null) { - spoiler_button = new Button.from_icon_name ("mail-attachment-symbolic", IconSize.BUTTON); - spoiler_button.label = spoiler_button_text; - spoiler_button.always_show_image = true; - content_label.margin_top = 6; - } - else { - spoiler_button = new Button.with_label (spoiler_button_text); - } - spoiler_button.hexpand = true; - spoiler_button.halign = Align.END; - spoiler_button.clicked.connect (() => revealer.set_reveal_child (!revealer.child_revealed)); + status.formal.bind_property ("reblogged", reblog_button, "active", BindingFlags.SYNC_CREATE); + reblog_button.clicked.connect (() => { + status.action (status.formal.reblogged ? "unreblog" : "reblog"); + }); - var spoiler_text = _("[ This post contains sensitive content ]"); - if (status.spoiler_text != null) - spoiler_text = status.spoiler_text; - content_spoiler = new Widgets.RichLabel (spoiler_text); - content_spoiler.wrap_words (); + reply_button.clicked.connect (() => new Dialogs.Compose.reply (status)); - spoiler_box.add (content_spoiler); - spoiler_box.add (spoiler_button); - spoiler_box.show_all (); - grid.attach (spoiler_box, 2, 3, 1, 1); + bind_property ("escaped-spoiler", content, "label", BindingFlags.SYNC_CREATE); + bind_property ("escaped-content", revealer_content, "label", BindingFlags.SYNC_CREATE); + status.formal.account.bind_property ("avatar", avatar, "url", BindingFlags.SYNC_CREATE); + bind_property ("handle", handle_label, "label", BindingFlags.SYNC_CREATE); + bind_property ("date", date_label, "label", BindingFlags.SYNC_CREATE); + status.formal.bind_property ("pinned", pin_indicator, "visible", BindingFlags.SYNC_CREATE); + status.formal.bind_property ("replies-count", reply_button, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + target.set_string (((int64)src).to_string ()); + return true; + }); + status.formal.bind_property ("reblogs-count", reblog_button, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + target.set_string (((int64)src).to_string ()); + return true; + }); + status.bind_property ("favourites-count", favorite_button, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => { + target.set_string (((int64)src).to_string ()); + return true; + }); + + status.formal.bind_property ("has_spoiler", revealer_content, "visible", BindingFlags.SYNC_CREATE); + revealer.reveal_child = !status.formal.has_spoiler; + + if (status.formal.visibility == API.Visibility.DIRECT) { + reblog_icon.icon_name = status.formal.visibility.get_icon (); + reblog_button.sensitive = false; + reblog_button.tooltip_text = _("This post can't be boosted"); } - if (!is_notification && status.get_formal ().attachments != null) - attachments.pack (status.get_formal ().attachments); - else + if (status.id <= 0) { + actions.destroy (); + date_label.destroy (); + content.single_line_mode = true; + content.lines = 2; + content.ellipsize = Pango.EllipsizeMode.END; + button_press_event.connect (on_avatar_clicked); + } + else { + button_press_event.connect (open); + } + + if (!attachments.populate (status.formal.attachments) || status.id <= 0) { attachments.destroy (); - - destroy.connect (() => { - avatar.show_default (AVATAR_SIZE); - if (separator != null) - separator.destroy (); - }); - - network.status_removed.connect (id => { - if (id == status.id) - destroy (); - }); - - rebind (); - } - - public void highlight () { - grid.get_style_context ().add_class ("card"); - grid.margin_bottom = 6; - } - - public int get_avatar_size () { - return AVATAR_SIZE * get_style_context ().get_scale (); - } - - public void rebind () { - var formal = status.get_formal (); - - title_user.set_label ("%s".printf ((formal.account.display_name))); - title_acct.label = "@" + formal.account.acct; - content_label.set_label (formal.content); - content_label.mentions = formal.mentions; - pin_indicator.visible = status.pinned; - - var datetime = parse_date_iso8601 (formal.created_at); - title_date.label = Granite.DateTime.get_relative_datetime (datetime); - - reblogs.label = formal.reblogs_count.to_string (); - favorites.label = formal.favourites_count.to_string (); - replies.label = formal.replies_count.to_string (); - - reblog.sensitive = false; - reblog.active = formal.reblogged; - reblog.sensitive = true; - favorite.sensitive = false; - favorite.active = formal.favorited; - favorite.sensitive = true; - - if (formal.visibility == API.StatusVisibility.DIRECT) { - reblog.sensitive = false; - reblog.icon.icon_name = formal.visibility.get_icon (); - reblog.tooltip_text = _("This post can't be boosted"); } - - network.load_avatar (formal.account.avatar, avatar, get_avatar_size ()); } - private GLib.DateTime? parse_date_iso8601 (string date) { - var timeval = GLib.TimeVal (); - if (timeval.from_iso8601 (date)) - return new GLib.DateTime.from_timeval_local (timeval); + public Status (API.Status status, API.NotificationType? _kind = null) { + Object (status: status, kind: _kind); + } - return null; + ~Status () { + button_press_event.disconnect (on_clicked); + streams.status_removed.disconnect (on_status_removed); + notify["kind"].disconnect (on_kind_changed); + } + + protected virtual void on_status_removed (int64 id) { + if (id == status.id) + destroy (); + } + + protected bool on_toggle_spoiler (string uri) { + if (uri == "tootle://toggle") { + revealer.reveal_child = !revealer.reveal_child; + return true; + } + return false; + } + + protected virtual void on_kind_changed () { + header_icon.visible = header_label.visible = (kind != null); + if (kind == null) + return; + + header_icon.icon_name = kind.get_icon (); + header_label.label = kind.get_desc (status.account); } public bool on_avatar_clicked (EventButton ev) { if (ev.button == 1) { - var view = new Views.Profile (status.get_formal ().account); + var view = new Views.Profile (status.formal.account); return window.open_view (view); } return false; @@ -253,7 +193,7 @@ public class Tootle.Widgets.Status : EventBox { public bool open (EventButton ev) { if (ev.button == 1) { - var formal = status.get_formal (); + var formal = status.formal; var view = new Views.ExpandedStatus (formal); return window.open_view (view); } @@ -264,30 +204,26 @@ public class Tootle.Widgets.Status : EventBox { if (ev.button == 3) return open_menu (ev.button, ev.time); return false; - } public virtual bool open_menu (uint button, uint32 time) { var menu = new Gtk.Menu (); - var is_muted = status.muted; - var is_pinned = status.pinned; - - var item_muting = new Gtk.MenuItem.with_label (is_muted ? _("Unmute Conversation") : _("Mute Conversation")); - item_muting.activate.connect (() => status.set_muted (!is_muted)); var item_open_link = new Gtk.MenuItem.with_label (_("Open in Browser")); - item_open_link.activate.connect (() => Desktop.open_uri (status.get_formal ().url)); + item_open_link.activate.connect (() => Desktop.open_uri (status.formal.url)); var item_copy_link = new Gtk.MenuItem.with_label (_("Copy Link")); - item_copy_link.activate.connect (() => Desktop.copy (status.get_formal ().url)); + item_copy_link.activate.connect (() => Desktop.copy (status.formal.url)); var item_copy = new Gtk.MenuItem.with_label (_("Copy Text")); item_copy.activate.connect (() => { - var sanitized = Html.remove_tags (status.get_formal ().content); + var sanitized = Html.remove_tags (status.formal.content); Desktop.copy (sanitized); }); if (status.is_owned ()) { - var item_pin = new Gtk.MenuItem.with_label (is_pinned ? _("Unpin from Profile") : _("Pin on Profile")); - item_pin.activate.connect (() => status.set_pinned (!is_pinned)); + var item_pin = new Gtk.MenuItem.with_label (status.pinned ? _("Unpin from Profile") : _("Pin on Profile")); + item_pin.activate.connect (() => { + status.action (status.formal.pinned ? "unpin" : "pin"); + }); menu.add (item_pin); var item_delete = new Gtk.MenuItem.with_label (_("Delete")); @@ -295,14 +231,17 @@ public class Tootle.Widgets.Status : EventBox { menu.add (item_delete); var item_redraft = new Gtk.MenuItem.with_label (_("Redraft")); - item_redraft.activate.connect (() => Dialogs.Compose.redraft (status.get_formal ())); + item_redraft.activate.connect (() => new Dialogs.Compose.redraft (status.formal)); menu.add (item_redraft); menu.add (new SeparatorMenuItem ()); } - if (is_notification) - menu.add (item_muting); + // if (is_notification) { + // var item_muting = new Gtk.MenuItem.with_label (status.muted ? _("Unmute Conversation") : _("Mute Conversation")); + // item_muting.activate.connect (() => status.update_muted (!is_muted) ); + // menu.add (item_muting); + // } menu.add (item_open_link); menu.add (new SeparatorMenuItem ()); diff --git a/src/Widgets/VisibilityPopover.vala b/src/Widgets/VisibilityPopover.vala new file mode 100644 index 0000000..5072a61 --- /dev/null +++ b/src/Widgets/VisibilityPopover.vala @@ -0,0 +1,48 @@ +using Gtk; + +public class Tootle.Widgets.VisibilityPopover: Popover { + + protected RadioButton? group_owner; + protected MenuButton button; + protected int i = 0; + + public API.Visibility selected { get; set; default = API.Visibility.PUBLIC; } + + protected Box box; + + construct { + var box = new Box (Orientation.VERTICAL, 8); + box.margin = 8; + box.show (); + add (box); + + foreach (API.Visibility item in API.Visibility.all ()){ + var radio = new RadioButton.from_widget (group_owner); + if (group_owner == null) + group_owner = radio; + + box.pack_start (radio, true, true, 0); + radio.toggled.connect (() => { + selected = item; + popdown (); + }); + + var label = new Label (@"$(item.get_name())\n$(item.get_desc())"); + label.use_markup = true; + label.xalign = 0; + label.margin_start = 8; + radio.add (label); + + i++; + } + + box.show_all (); + } + + public VisibilityPopover.with_button (MenuButton w) { + button = w; + button.popover = this; + } + +} +