Merge refactor branch (#173)

This commit is contained in:
Bleak Grey 2020-05-29 15:19:35 +03:00 committed by GitHub
parent d8fc951fb0
commit 55d566140c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 8692 additions and 7179 deletions

1
.gitignore vendored
View File

@ -2,6 +2,5 @@ _ignore
build
build.sh
build-po.sh
install.sh
uninstall.sh
*~

View File

@ -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)
<a href='https://flathub.org/apps/details/com.github.bleakgrey.tootle'><img height='51' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a>
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
<a href="https://liberapay.com/bleakgrey/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
## 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)

View File

@ -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;
}

View File

@ -6,7 +6,7 @@
<project_license>GPL-3.0+</project_license>
<name>Tootle</name>
<summary>Lightning fast client for Mastodon</summary>
<description>
<p>
Tootle is a client for the worlds 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.
</p>
</description>
<provides>
<binary>com.github.bleakgrey.tootle</binary>
</provides>
<developer_name>bleak_grey</developer_name>
<url type="homepage">https://github.com/bleakgrey</url>
<url type="bugtracker">https://github.com/bleakgrey/tootle/issues</url>
<url type="donation">https://liberapay.com/bleakgrey/donate</url>
<content_rating type="oars-1.1">
<content_attribute id="violence-cartoon">none</content_attribute>
<content_attribute id="violence-fantasy">none</content_attribute>
@ -57,20 +57,11 @@
<content_attribute id="money-purchasing">none</content_attribute>
<content_attribute id="money-gambling">none</content_attribute>
</content_rating>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot2.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot3.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/bleakgrey/tootle/master/data/screenshot4.png</image>
</screenshot>
</screenshots>
<releases>
@ -111,7 +102,7 @@
</description>
</release>
</releases>
<custom>
<value key="x-appcenter-color-primary">#F5F8FF</value>
<value key="x-appcenter-color-primary-text">#413F58</value>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/github/bleakgrey/tootle/">
<file alias="app.css">app.css</file>
<file alias="light.css">light.css</file>
<file alias="dark.css">dark.css</file>
<file alias="logo128">logo128.png</file>
<file alias="empty_state">empty_state.png</file>
</gresource>
</gresources>

View File

@ -16,16 +16,6 @@
<summary>Always monitor new notifications</summary>
<description></description>
</key>
<key name="cache" type="b">
<default>false</default>
<summary>Cache images to reduce network load</summary>
<description></description>
</key>
<key name="cache-size" type="i">
<default>64</default>
<summary>Cache size</summary>
<description>Sets the maximum size of cached content</description>
</key>
<key name="live-updates" type="b">
<default>true</default>
<summary>Real-time timelines</summary>

View File

@ -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);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

14
data/gresource.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/github/bleakgrey/tootle/">
<file alias="app.css">app.css</file>
<file preprocess="xml-stripblanks">ui/views/new_account.ui</file>
<file preprocess="xml-stripblanks">ui/views/base.ui</file>
<file preprocess="xml-stripblanks">ui/views/profile_header.ui</file>
<file preprocess="xml-stripblanks">ui/widgets/status.ui</file>
<file preprocess="xml-stripblanks">ui/widgets/accounts_button.ui</file>
<file preprocess="xml-stripblanks">ui/widgets/accounts_button_item.ui</file>
<file preprocess="xml-stripblanks">ui/dialogs/compose.ui</file>
<file preprocess="xml-stripblanks">ui/dialogs/main.ui</file>
</gresource>
</gresources>

View File

@ -1,188 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg4379"
sodipodi:docname="128x128.svg"
inkscape:version="0.91 r13725"
x="0px"
y="0px"
width="128px"
height="128px"
viewBox="0 0 128 128"
enable-background="new 0 0 128 128"
xml:space="preserve"><metadata
id="metadata59"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs57" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="705"
id="namedview55"
showgrid="false"
inkscape:zoom="1.84375"
inkscape:cx="54.508475"
inkscape:cy="64"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="svg4379" /><radialGradient
id="path3041_1_"
cx="-292.7208"
cy="1871.6008"
r="35.3381"
gradientTransform="matrix(1.5564 0 0 -0.1698 519.5884 435.7747)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#000000"
id="stop4" /><stop
offset="1"
style="stop-color:#000000;stop-opacity:0"
id="stop6" /></radialGradient><path
id="path3041"
opacity="0.2"
fill="url(#path3041_1_)"
enable-background="new "
d="M119,118 c-0.006,3.314-24.635,5.999-55.01,5.999C33.622,123.998,9.006,121.313,9,118c-0.006-3.314,24.614-6.001,54.99-6.001 c30.376-0.001,55.005,2.685,55.01,5.999C119,117.999,119,117.999,119,118z" /><g
id="g2036"
transform="matrix(2.6999989,0,0,0.55555607,-0.8000019,94.888882)"><g
id="g3712"
transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)"
opacity="0.4"><radialGradient
id="rect2801_1_"
cx="-136.6434"
cy="5007.957"
r="2.4994"
gradientTransform="matrix(5.695 0 0 -1 816.1725 5051.457)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop12" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop14" /></radialGradient><rect
id="rect2801"
x="38"
y="40"
fill="url(#rect2801_1_)"
width="5"
height="7" /><radialGradient
id="rect3696_1_"
cx="-165.4541"
cy="6006.0449"
r="2.4994"
gradientTransform="matrix(-5.695 0 0 1 -932.2481 -5962.5449)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop18" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop20" /></radialGradient><rect
id="rect3696"
x="5"
y="40"
fill="url(#rect3696_1_)"
width="5"
height="7" /><linearGradient
id="rect3700_1_"
gradientUnits="userSpaceOnUse"
x1="-115.496"
y1="4803.4004"
x2="-115.496"
y2="4810.4287"
gradientTransform="matrix(2.8421 0 0 -0.7143 352.2516 3478.0276)"><stop
offset="0"
style="stop-color:#181818;stop-opacity:0"
id="stop24" /><stop
offset="0.5"
style="stop-color:#181818"
id="stop26" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop28" /></linearGradient><rect
id="rect3700"
x="10"
y="40"
fill="url(#rect3700_1_)"
width="28"
height="7" /></g></g><radialGradient
id="SVGID_1_"
cx="-42.2709"
cy="69.1949"
r="322.7202"
fx="-55.7745"
fy="69.1949"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#9BAEC8"
id="stop32" /><stop
offset="0.4576"
style="stop-color:#8093AA"
id="stop34" /><stop
offset="1"
style="stop-color:#273445"
id="stop36" /></radialGradient><path
fill="url(#SVGID_1_)"
d="M115.5,21.55v90.9c0,3.34-2.71,6.05-6.05,6.05h-90.9c-3.34,0-6.05-2.71-6.05-6.05v-90.9 c0-3.34,2.71-6.05,6.05-6.05h90.9C112.79,15.5,115.5,18.21,115.5,21.55z"
id="path38" /><linearGradient
id="rect6741-7_1_"
gradientUnits="userSpaceOnUse"
x1="-329.085"
y1="3929.1445"
x2="-329.085"
y2="3891.1445"
gradientTransform="matrix(2.7297 0 0 -2.7297 962.313 10740.0137)"><stop
offset="0"
style="stop-color:#FFFFFF"
id="stop41" /><stop
offset="0.0632"
style="stop-color:#FFFFFF;stop-opacity:0.2353"
id="stop43" /><stop
offset="0.9506"
style="stop-color:#FFFFFF;stop-opacity:0.1569"
id="stop45" /><stop
offset="1"
style="stop-color:#FFFFFF;stop-opacity:0.3922"
id="stop47" /></linearGradient><path
id="rect6741-7"
opacity="0.3"
fill="none"
stroke="url(#rect6741-7_1_)"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M18.5,15.875h91c2.761,0,5,2.239,5,5v91c0,2.761-2.239,5-5,5h-91c-2.761,0-5-2.239-5-5v-91C13.5,18.114,15.739,15.875,18.5,15.875z" /><path
id="rect5505-21-6"
opacity="0.5"
fill="none"
stroke="#0E141F"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M18.5,15.5h91c3.314,0,6,2.686,6,6v91c0,3.314-2.686,6-6,6h-91c-3.314,0-6-2.686-6-6v-91C12.5,18.186,15.186,15.5,18.5,15.5z" /><path
d="M115.5,50.65v61.8c0,3.34-2.71,6.05-6.05,6.05H51.31L24.13,92.02l-0.08-0.08v-0.01 c-0.02-0.02-0.03-0.04-0.05-0.05l-5.5-10.54c-0.12-0.31,0.01-0.66,0.31-0.82c0.14-0.07,0.29-0.09,0.42-0.06c0.02,0,0.03,0,0.04,0.01 l5.02,1.09l1.18-0.62l0.89-0.46l12.63-6.6c0.08-0.04,0.17-0.09,0.25-0.13l6.18-3.22l-1-1.91c-0.41-0.79-0.29-1.72,0.23-2.38 l-3.46-3.34c-0.1-0.08-0.19-0.17-0.28-0.27c-0.17-0.19-0.31-0.39-0.43-0.62c-0.77-1.47-0.2-3.27,1.27-4.04l16.81-8.77 c1.11-0.58,2.42-0.39,3.32,0.38l0.26,0.25l8.04,7.77l4.36-2.27c0.08-0.05,0.17-0.09,0.25-0.14l12.83-6.7l6.47-15.51 c0.01-0.03,0.02-0.06,0.04-0.08c0.12-0.28,0.34-0.52,0.63-0.68c0.5-0.26,1.08-0.2,1.51,0.11L115.5,50.65z"
id="path51"
fill="#273445"
opacity="0.5"
style="opacity:0.15" /><path
fill="#FFFFFF"
d="M108.571,55.606l-11.964-22.92c-0.018-0.027-0.034-0.051-0.055-0.075c-0.082-0.106-0.178-0.199-0.284-0.275 c-0.426-0.311-1.007-0.375-1.507-0.114c-0.293,0.155-0.511,0.396-0.636,0.678c-0.018,0.025-0.026,0.054-0.037,0.083l-6.467,15.511 l-12.831,6.699c-0.084,0.044-0.171,0.088-0.251,0.134l-4.363,2.278l-3.801,1.984l-0.994-1.905c-0.564-1.079-1.89-1.496-2.969-0.932 c-1.077,0.562-1.496,1.89-0.932,2.969l0.994,1.905l-3.672,1.917l-3.172-6.075l5.693-2.971c1.461-0.765,2.029-2.571,1.266-4.032 c-0.123-0.238-0.274-0.45-0.449-0.638l-0.259-0.251c-0.899-0.769-2.213-0.958-3.323-0.378l-16.812,8.775 c-1.463,0.765-2.031,2.571-1.268,4.034c0.12,0.229,0.265,0.437,0.433,0.62c0.087,0.099,0.182,0.192,0.282,0.273 c0.899,0.764,2.208,0.951,3.318,0.371l5.691-2.969l3.17,6.075l-4.054,2.116l-0.994-1.905c-0.564-1.077-1.894-1.494-2.969-0.932 c-0.284,0.148-0.521,0.349-0.709,0.585c-0.52,0.66-0.639,1.589-0.225,2.384l0.994,1.905l-6.172,3.223 c-0.084,0.042-0.169,0.086-0.253,0.13l-14.7,7.674l-5.016-1.089c-0.014-0.004-0.027-0.006-0.038-0.008 c-0.139-0.027-0.289-0.006-0.424,0.063c-0.3,0.157-0.428,0.511-0.309,0.818l5.501,10.535c0.014,0.02,0.028,0.04,0.043,0.058 l0.007,0.009l0.08,0.075c0.195,0.162,0.476,0.198,0.718,0.073c0.135-0.072,0.234-0.181,0.293-0.312 c0.007-0.011,0.011-0.023,0.015-0.036l2.014-4.828l5.884-3.073c0.023,1.672,0.427,3.363,1.253,4.946 c2.823,5.406,9.497,7.502,14.906,4.679c0.084-0.044,0.169-0.088,0.251-0.134l6.172-3.223l1.154,2.211 c0.562,1.077,1.894,1.494,2.969,0.932c1.077-0.562,1.496-1.892,0.934-2.969l-1.154-2.209l4.054-2.118l3.367,6.448 c0.784,1.501,2.632,2.082,4.133,1.298c0.42-0.217,0.768-0.521,1.029-0.875c0.688-0.914,0.831-2.177,0.267-3.258l-3.364-6.448 l3.672-1.917l1.154,2.209c0.562,1.077,1.892,1.496,2.969,0.934c1.079-0.564,1.494-1.894,0.932-2.969l-1.154-2.211l8.517-4.446 c0.588-0.314,1.135-0.675,1.642-1.075c4.085-3.225,5.435-8.995,2.94-13.776c-0.825-1.581-1.982-2.879-3.34-3.855l4.014-2.095 l16.518,3.58c0.031,0.008,0.059,0.017,0.088,0.019c0.305,0.058,0.627,0.019,0.92-0.136C108.55,57.044,108.829,56.275,108.571,55.606 z M46.51,88.011c-0.038,0.023-0.073,0.042-0.113,0.062c-2.431,1.269-5.427,0.329-6.697-2.102c-1.267-2.429-0.329-5.425,2.098-6.694 l6.432-3.357l4.592,8.797L46.51,88.011z M56.724,82.679l-4.592-8.797l0.269-0.139l3.786-1.977l4.592,8.797l-0.864,0.451 L56.724,82.679z M66.209,77.729l-4.592-8.797l3.672-1.917l4.592,8.797L66.209,77.729z M81.444,69.766 c-0.053,0.034-0.109,0.067-0.164,0.095l-7.498,3.915l-4.592-8.797l5.591-2.92l1.792-0.934c0.095-0.049,0.191-0.095,0.289-0.139 c2.374-1.056,5.189-0.094,6.408,2.241c1.075,2.056,0.567,4.516-1.086,5.995C81.958,69.421,81.712,69.605,81.444,69.766z"
id="path53" /></svg>

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -1,125 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg4372"
inkscape:version="0.91 r13725"
sodipodi:docname="16x16.svg"
x="0px"
y="0px"
width="16px"
height="16px"
viewBox="0 0 16 16"
enable-background="new 0 0 16 16"
xml:space="preserve"><metadata
id="metadata34"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs32" /><sodipodi:namedview
inkscape:zoom="32"
objecttolerance="10"
bordercolor="#666666"
borderopacity="1"
gridtolerance="10"
guidetolerance="10"
showgrid="true"
pagecolor="#ffffff"
inkscape:cx="5.6537797"
inkscape:cy="7.7070694"
id="namedview23"
inkscape:current-layer="svg4372"
inkscape:window-height="705"
inkscape:window-y="30"
inkscape:window-x="0"
inkscape:window-maximized="1"
inkscape:pageopacity="0"
inkscape:window-width="1366"
inkscape:pageshadow="2"><inkscape:grid
type="xygrid"
empspacing="4"
id="grid4151" /><inkscape:grid
type="xygrid"
color="#ff00ff"
opacity="0.2627451"
spacingy="0.5"
empopacity="0"
empcolor="#ff3fff"
empspacing="4"
spacingx="0.5"
id="grid4153" /></sodipodi:namedview><radialGradient
id="SVGID_1_"
cx="-5.4128"
cy="8.277"
r="40.7317"
fx="-7.1172"
fy="8.277"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#95A3AB"
id="stop7" /><stop
offset="0.2624"
style="stop-color:#9BAEC8"
id="stop9" /><stop
offset="0.705"
style="stop-color:#8093AA"
id="stop11" /><stop
offset="1"
style="stop-color:#273445"
id="stop13" /></radialGradient><path
fill="url(#SVGID_1_)"
d="M14.5,2.5v11c0,0.52-0.4,0.94-0.9,0.99c-0.03,0.01-0.07,0.01-0.1,0.01h-11c-0.55,0-1-0.45-1-1v-11 c0-0.55,0.45-1,1-1h11C14.05,1.5,14.5,1.95,14.5,2.5z"
id="path15" /><linearGradient
id="rect6741-0-3-5_1_"
gradientUnits="userSpaceOnUse"
x1="502.1291"
y1="2616.1348"
x2="502.1291"
y2="2578.1348"
gradientTransform="matrix(0.2973 0 0 -0.2973 -141.2816 780.1212)"><stop
offset="0"
style="stop-color:#FFFFFF"
id="stop18" /><stop
offset="0.0632"
style="stop-color:#FFFFFF;stop-opacity:0.2353"
id="stop20" /><stop
offset="0.9506"
style="stop-color:#FFFFFF;stop-opacity:0.1569"
id="stop22" /><stop
offset="1"
style="stop-color:#FFFFFF;stop-opacity:0.3922"
id="stop24" /></linearGradient><rect
id="rect6741-0-3-5"
x="2.5"
y="2.5"
opacity="0.3"
fill="none"
stroke="url(#rect6741-0-3-5_1_)"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
width="11"
height="11" /><path
id="rect5505-21-2-8"
opacity="0.5"
fill="none"
stroke="#0E141F"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M2.5,1.5h11c0.552,0,1,0.448,1,1v11c0,0.552-0.448,1-1,1h-11c-0.552,0-1-0.448-1-1v-11C1.5,1.948,1.948,1.5,2.5,1.5z" /><path
opacity="0.5"
fill="#273445"
d="M14.5,6.56v6.94c0,0.52-0.4,0.94-0.9,0.99H6.77l-3.23-3.14v-0.01c-0.01,0-0.01,0-0.01,0 l-0.3-0.58l-0.35-0.68c-0.01-0.03,0-0.07,0.04-0.09c0.02-0.01,0.03-0.01,0.05-0.01l0.26,0.06l0.33,0.07l0.15-0.07l0.11-0.06 l1.49-0.77c0.01-0.02,0.02-0.02,0.03-0.02l0.73-0.38L5.95,8.58C5.9,8.48,5.92,8.37,5.98,8.29L5.57,7.9L5.54,7.87 C5.52,7.85,5.5,7.82,5.48,7.79C5.4,7.62,5.46,7.41,5.63,7.32l2-1.04c0.13-0.07,0.29-0.05,0.4,0.05l0.02,0.02l0.96,0.92l0.51-0.26 C9.53,7,9.54,7,9.55,6.99l1.52-0.8l0.77-1.84c0.01-0.04,0.05-0.06,0.07-0.09c0.06-0.03,0.14-0.02,0.19,0.02L14.5,6.56z"
id="path28"
style="opacity:0.15" /><path
fill="#FFFFFF"
d="M13.555,7.038l-1.418-2.716c-0.002-0.003-0.004-0.006-0.007-0.009c-0.01-0.013-0.021-0.024-0.034-0.033 c-0.05-0.037-0.119-0.044-0.179-0.014c-0.035,0.018-0.061,0.047-0.075,0.08c-0.002,0.003-0.003,0.006-0.004,0.01l-0.766,1.838 L9.551,6.989c-0.01,0.005-0.02,0.01-0.03,0.016l-0.517,0.27L8.554,7.51L8.436,7.284c-0.067-0.128-0.224-0.177-0.352-0.11 C7.957,7.24,7.907,7.397,7.974,7.525l0.118,0.226L7.656,7.978l-0.376-0.72l0.675-0.352c0.173-0.091,0.24-0.305,0.15-0.478 C8.091,6.4,8.073,6.375,8.052,6.353l-0.031-0.03C7.915,6.232,7.759,6.21,7.627,6.278l-1.992,1.04 c-0.173,0.091-0.241,0.305-0.15,0.478C5.499,7.823,5.516,7.848,5.536,7.87c0.01,0.012,0.022,0.023,0.033,0.032 c0.107,0.091,0.262,0.113,0.393,0.044l0.674-0.352l0.376,0.72l-0.48,0.251L6.415,8.339C6.348,8.212,6.19,8.162,6.063,8.229 C6.029,8.246,6.001,8.27,5.979,8.298C5.917,8.376,5.903,8.486,5.952,8.581L6.07,8.806L5.338,9.188c-0.01,0.005-0.02,0.01-0.03,0.015 l-1.742,0.909L2.972,9.984C2.97,9.984,2.968,9.983,2.967,9.983C2.951,9.98,2.933,9.982,2.917,9.991 c-0.036,0.019-0.051,0.061-0.037,0.097l0.652,1.249c0.002,0.002,0.003,0.005,0.005,0.007l0.001,0.001l0.009,0.009 c0.023,0.019,0.056,0.023,0.085,0.009c0.016-0.008,0.028-0.021,0.035-0.037c0.001-0.001,0.001-0.003,0.002-0.004l0.239-0.572 l0.697-0.364c0.003,0.198,0.051,0.399,0.148,0.586c0.335,0.641,1.126,0.889,1.767,0.555c0.01-0.005,0.02-0.01,0.03-0.016 l0.731-0.382l0.137,0.262c0.067,0.128,0.225,0.177,0.352,0.11c0.128-0.067,0.177-0.224,0.111-0.352l-0.137-0.262l0.48-0.251 l0.399,0.764c0.093,0.178,0.312,0.247,0.49,0.154c0.05-0.026,0.091-0.062,0.122-0.104c0.082-0.108,0.098-0.258,0.032-0.386 l-0.399-0.764l0.435-0.227l0.137,0.262c0.067,0.128,0.224,0.177,0.352,0.111c0.128-0.067,0.177-0.225,0.11-0.352L9.765,9.83 l1.009-0.527c0.07-0.037,0.135-0.08,0.195-0.127c0.484-0.382,0.644-1.066,0.348-1.633c-0.098-0.187-0.235-0.341-0.396-0.457 l0.476-0.248l1.958,0.424c0.004,0.001,0.007,0.002,0.01,0.002c0.036,0.007,0.074,0.002,0.109-0.016 C13.552,7.208,13.585,7.117,13.555,7.038z M6.2,10.878c-0.004,0.003-0.009,0.005-0.013,0.007c-0.288,0.15-0.643,0.039-0.794-0.249 c-0.15-0.288-0.039-0.643,0.249-0.793l0.762-0.398l0.544,1.043L6.2,10.878z M7.41,10.246L6.866,9.204l0.032-0.016l0.449-0.234 l0.544,1.043l-0.102,0.053L7.41,10.246z M8.534,9.66L7.99,8.617L8.425,8.39l0.544,1.043L8.534,9.66z M10.34,8.716 c-0.006,0.004-0.013,0.008-0.019,0.011L9.432,9.191L8.888,8.148L9.55,7.802l0.212-0.111C9.774,7.686,9.785,7.68,9.797,7.675 c0.281-0.125,0.615-0.011,0.759,0.266c0.127,0.244,0.067,0.535-0.129,0.71C10.401,8.675,10.372,8.697,10.34,8.716z"
id="path30" /></svg>

Before

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -1,189 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg4157"
inkscape:version="0.91 r13725"
sodipodi:docname="24x24.svg"
x="0px"
y="0px"
width="24px"
height="24px"
viewBox="0 0 24 24"
enable-background="new 0 0 24 24"
xml:space="preserve"><metadata
id="metadata56"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs54" /><sodipodi:namedview
inkscape:zoom="16"
objecttolerance="10"
bordercolor="#666666"
borderopacity="1"
gridtolerance="10"
guidetolerance="10"
showgrid="true"
pagecolor="#ffffff"
inkscape:cx="12.702408"
inkscape:cy="9.3530973"
id="namedview46"
inkscape:current-layer="svg4157"
inkscape:window-height="705"
inkscape:window-y="30"
inkscape:window-x="0"
inkscape:window-maximized="1"
inkscape:pageopacity="0"
inkscape:window-width="1366"
inkscape:pageshadow="2"><inkscape:grid
type="xygrid"
empspacing="4"
id="grid4179" /><inkscape:grid
type="xygrid"
color="#ff3fff"
opacity="0.1254902"
spacingy="0.5"
empopacity="0.11764706"
empcolor="#3ff9ff"
empspacing="2"
spacingx="0.5"
id="grid4181" /></sodipodi:namedview><g
id="g2036-4"
transform="matrix(0.55,0,0,0.3333336,-1.2000011,7.33333)"><g
id="g3712-8"
transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)"
opacity="0.4"><radialGradient
id="rect2801-6_1_"
cx="-410.7503"
cy="7665.8701"
r="2.4994"
gradientTransform="matrix(1.1601 0 0 -0.6 514.4986 4643.021)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop9" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop11" /></radialGradient><rect
id="rect2801-6"
x="38"
y="40"
fill="url(#rect2801-6_1_)"
width="5"
height="7" /><radialGradient
id="rect3696-20_1_"
cx="-1259.9097"
cy="10842.7783"
r="2.4994"
gradientTransform="matrix(-1.1601 0 0 0.6 -1451.5964 -6462.1655)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop15" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop17" /></radialGradient><rect
id="rect3696-20"
x="5"
y="40"
fill="url(#rect3696-20_1_)"
width="5"
height="7" /><linearGradient
id="rect3700-5_1_"
gradientUnits="userSpaceOnUse"
x1="48.2663"
y1="7022.2568"
x2="48.2663"
y2="7029.2852"
gradientTransform="matrix(0.5789 0 0 -0.4286 -3.9436 3056.5657)"><stop
offset="0"
style="stop-color:#181818;stop-opacity:0"
id="stop21" /><stop
offset="0.5"
style="stop-color:#181818"
id="stop23" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop25" /></linearGradient><rect
id="rect3700-5"
x="10"
y="40"
fill="url(#rect3700-5_1_)"
width="28"
height="7" /></g></g><radialGradient
id="SVGID_1_"
cx="-7.6034"
cy="12.4049"
r="59.5309"
fx="-10.0943"
fy="12.4049"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#95A3AB"
id="stop29" /><stop
offset="0.2624"
style="stop-color:#9BAEC8"
id="stop31" /><stop
offset="0.705"
style="stop-color:#8093AA"
id="stop33" /><stop
offset="1"
style="stop-color:#273445"
id="stop35" /></radialGradient><path
fill="url(#SVGID_1_)"
d="M21.5,3.5v17c0,0.52-0.4,0.94-0.9,0.99c-0.03,0.01-0.07,0.01-0.1,0.01h-17c-0.55,0-1-0.45-1-1v-17 c0-0.55,0.45-1,1-1h17C21.05,2.5,21.5,2.95,21.5,3.5z"
id="path37" /><path
id="rect5505-21-8-1"
opacity="0.5"
fill="none"
stroke="#0E141F"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M3.5,2.5h17c0.552,0,1,0.448,1,1v17c0,0.552-0.448,1-1,1h-17c-0.552,0-1-0.448-1-1v-17C2.5,2.948,2.948,2.5,3.5,2.5z" /><linearGradient
id="rect6741-9_1_"
gradientUnits="userSpaceOnUse"
x1="154.1423"
y1="3116.9497"
x2="154.1423"
y2="3082.2695"
gradientTransform="matrix(0.4595 0 0 -0.4595 -58.8209 1436.0929)"><stop
offset="0"
style="stop-color:#FFFFFF"
id="stop41" /><stop
offset="0.0165"
style="stop-color:#FFFFFF;stop-opacity:0.2353"
id="stop43" /><stop
offset="0.98"
style="stop-color:#FFFFFF;stop-opacity:0.1569"
id="stop45" /><stop
offset="1"
style="stop-color:#FFFFFF;stop-opacity:0.3922"
id="stop47" /></linearGradient><rect
id="rect6741-9"
x="3.501"
y="3.499"
opacity="0.3"
fill="none"
stroke="url(#rect6741-9_1_)"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
width="17"
height="17" /><path
opacity="0.5"
fill="#273445"
d="M21.5,9.79V20.5c0,0.52-0.4,0.94-0.9,0.99H10.03l-4.77-4.64H5.25v-0.01 c-0.01-0.01-0.01-0.01-0.01-0.01l-0.45-0.85l-0.51-1c-0.02-0.05,0-0.11,0.05-0.14c0.03-0.02,0.05-0.02,0.08-0.02c0,0,0,0,0,0.01 l0.38,0.08l0.5,0.11l0.21-0.11l0.16-0.08l2.21-1.15c0.01-0.02,0.03-0.02,0.05-0.02L9,13.09l-0.18-0.34 c-0.07-0.14-0.05-0.3,0.04-0.42l-0.61-0.58c-0.01-0.01-0.03-0.03-0.04-0.05c-0.03-0.03-0.06-0.07-0.08-0.11 c-0.13-0.25-0.04-0.57,0.22-0.7l2.95-1.54c0.19-0.1,0.43-0.07,0.59,0.07l0.04,0.04l1.41,1.36l0.76-0.39 c0.02-0.02,0.03-0.02,0.05-0.03l2.24-1.18l1.14-2.72c0,0,0,0,0-0.01c0.02-0.05,0.07-0.09,0.11-0.12c0.09-0.05,0.2-0.04,0.27,0.02 L21.5,9.79z"
id="path50"
style="opacity:0.15" /><path
fill="#FFFFFF"
d="M20.068,10.472l-2.097-4.018c-0.003-0.005-0.006-0.009-0.01-0.013c-0.014-0.019-0.031-0.035-0.05-0.048 c-0.075-0.055-0.177-0.066-0.264-0.02c-0.051,0.027-0.09,0.069-0.112,0.119c-0.003,0.004-0.005,0.009-0.006,0.015l-1.134,2.719 l-2.25,1.174c-0.015,0.008-0.03,0.015-0.044,0.023l-0.765,0.399l-0.666,0.348l-0.174-0.334c-0.099-0.189-0.331-0.262-0.521-0.163 c-0.189,0.099-0.262,0.331-0.163,0.521l0.174,0.334l-0.644,0.336l-0.556-1.065l0.998-0.521c0.256-0.134,0.356-0.451,0.222-0.707 c-0.021-0.042-0.048-0.079-0.079-0.112l-0.045-0.044C11.725,9.28,11.495,9.247,11.3,9.349l-2.947,1.538 c-0.257,0.134-0.356,0.451-0.222,0.707c0.021,0.04,0.046,0.077,0.076,0.109c0.015,0.017,0.032,0.034,0.049,0.048 c0.158,0.134,0.387,0.167,0.582,0.065l0.998-0.521l0.556,1.065L9.68,12.731l-0.174-0.334c-0.099-0.189-0.332-0.262-0.521-0.163 c-0.05,0.026-0.091,0.061-0.124,0.103c-0.091,0.116-0.112,0.279-0.039,0.418l0.174,0.334l-1.082,0.565 c-0.015,0.007-0.03,0.015-0.044,0.023l-2.577,1.345l-0.879-0.191C4.41,14.83,4.408,14.83,4.406,14.829 c-0.024-0.005-0.051-0.001-0.074,0.011c-0.053,0.028-0.075,0.09-0.054,0.143l0.964,1.847c0.002,0.003,0.005,0.007,0.008,0.01 l0.001,0.002l0.014,0.013c0.034,0.028,0.083,0.035,0.126,0.013c0.024-0.013,0.041-0.032,0.051-0.055 c0.001-0.002,0.002-0.004,0.003-0.006l0.353-0.846l1.032-0.539c0.004,0.293,0.075,0.59,0.22,0.867 c0.495,0.948,1.665,1.315,2.613,0.82c0.015-0.008,0.03-0.015,0.044-0.023l1.082-0.565l0.202,0.388 c0.099,0.189,0.332,0.262,0.521,0.163c0.189-0.099,0.262-0.332,0.164-0.521l-0.202-0.387l0.711-0.371l0.59,1.13 c0.137,0.263,0.461,0.365,0.725,0.228c0.074-0.038,0.135-0.091,0.18-0.153c0.121-0.16,0.146-0.382,0.047-0.571l-0.59-1.13 l0.644-0.336l0.202,0.387c0.099,0.189,0.332,0.262,0.521,0.164c0.189-0.099,0.262-0.332,0.163-0.521l-0.202-0.388l1.493-0.78 c0.103-0.055,0.199-0.118,0.288-0.188c0.716-0.565,0.953-1.577,0.515-2.415c-0.145-0.277-0.348-0.505-0.586-0.676l0.704-0.367 l2.896,0.628c0.006,0.001,0.01,0.003,0.015,0.003c0.054,0.01,0.11,0.003,0.161-0.024C20.065,10.724,20.114,10.589,20.068,10.472z M9.188,16.153c-0.007,0.004-0.013,0.007-0.02,0.011c-0.426,0.223-0.952,0.058-1.174-0.369c-0.222-0.426-0.058-0.951,0.368-1.174 l1.128-0.589l0.805,1.542L9.188,16.153z M10.978,15.219l-0.805-1.542l0.047-0.024l0.664-0.347l0.805,1.542l-0.151,0.079 L10.978,15.219z M12.641,14.351l-0.805-1.542l0.644-0.336l0.805,1.542L12.641,14.351z M15.312,12.955 c-0.009,0.006-0.019,0.012-0.029,0.017l-1.315,0.686l-0.805-1.542l0.98-0.512l0.314-0.164c0.017-0.009,0.033-0.017,0.051-0.024 c0.416-0.185,0.91-0.017,1.123,0.393c0.188,0.36,0.099,0.792-0.19,1.051C15.402,12.894,15.359,12.926,15.312,12.955z"
id="path52" /></svg>

Before

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -1,185 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg4588"
inkscape:version="0.91 r13725"
sodipodi:docname="32x32.svg"
x="0px"
y="0px"
width="32px"
height="32px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"><metadata
id="metadata56"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs54" /><sodipodi:namedview
inkscape:zoom="16"
objecttolerance="10"
bordercolor="#666666"
borderopacity="1"
gridtolerance="10"
guidetolerance="10"
showgrid="true"
pagecolor="#ffffff"
inkscape:cx="13.84291"
inkscape:cy="15.588816"
id="namedview46"
inkscape:current-layer="svg4588"
inkscape:window-height="705"
inkscape:window-y="30"
inkscape:window-x="0"
inkscape:window-maximized="1"
inkscape:pageopacity="0"
inkscape:window-width="1366"
inkscape:pageshadow="2"><inkscape:grid
type="xygrid"
empspacing="4"
id="grid4203" /><inkscape:grid
type="xygrid"
color="#ff3fff"
opacity="0.37254902"
spacingy="0.5"
empopacity="0.1254902"
empcolor="#3fffff"
empspacing="2"
spacingx="0.5"
id="grid4205" /></sodipodi:namedview><g
id="g2036-2"
transform="matrix(0.6999997,0,0,0.3333336,-0.8000002,15.33333)"><g
id="g3712-3"
transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)"
opacity="0.4"><radialGradient
id="rect2801-0_1_"
cx="-392.7515"
cy="7672.0923"
r="2.4994"
gradientTransform="matrix(1.4765 0 0 -0.6 617.8796 4646.7544)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop9" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop11" /></radialGradient><rect
id="rect2801-0"
x="38"
y="40"
fill="url(#rect2801-0_1_)"
width="5"
height="7" /><radialGradient
id="rect3696-2_1_"
cx="-909.6242"
cy="10817.8896"
r="2.4994"
gradientTransform="matrix(-1.4765 0 0 0.6 -1333.028 -6447.2324)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop15" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop17" /></radialGradient><rect
id="rect3696-2"
x="5"
y="40"
fill="url(#rect3696-2_1_)"
width="5"
height="7" /><linearGradient
id="rect3700-1_1_"
gradientUnits="userSpaceOnUse"
x1="-107.542"
y1="7034.7012"
x2="-107.542"
y2="7041.7295"
gradientTransform="matrix(0.7368 0 0 -0.4286 103.2414 3061.8992)"><stop
offset="0"
style="stop-color:#181818;stop-opacity:0"
id="stop21" /><stop
offset="0.5"
style="stop-color:#181818"
id="stop23" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop25" /></linearGradient><rect
id="rect3700-1"
x="10"
y="40"
fill="url(#rect3700-1_1_)"
width="28"
height="7" /></g></g><radialGradient
id="SVGID_1_"
cx="-11.8574"
cy="16.5754"
r="84.5966"
fx="-15.3972"
fy="16.5754"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#95A3AB"
id="stop29" /><stop
offset="0.2624"
style="stop-color:#9BAEC8"
id="stop31" /><stop
offset="0.705"
style="stop-color:#8093AA"
id="stop33" /><stop
offset="1"
style="stop-color:#273445"
id="stop35" /></radialGradient><path
fill="url(#SVGID_1_)"
d="M29.5,4.66v22.68c0,1.06-0.78,1.95-1.79,2.13c-0.12,0.02-0.24,0.03-0.37,0.03H4.66 c-1.19,0-2.16-0.97-2.16-2.16V4.66c0-1.19,0.97-2.16,2.16-2.16h22.68C28.53,2.5,29.5,3.47,29.5,4.66z"
id="path37" /><linearGradient
id="rect6741-7-4_1_"
gradientUnits="userSpaceOnUse"
x1="-46.3832"
y1="3411.5254"
x2="-46.3832"
y2="3376.2197"
gradientTransform="matrix(0.6757 0 0 -0.6757 47.34 2309.1753)"><stop
offset="0"
style="stop-color:#FFFFFF"
id="stop40" /><stop
offset="0.038"
style="stop-color:#FFFFFF;stop-opacity:0.2353"
id="stop42" /><stop
offset="0.962"
style="stop-color:#FFFFFF;stop-opacity:0.1569"
id="stop44" /><stop
offset="1"
style="stop-color:#FFFFFF;stop-opacity:0.3922"
id="stop46" /></linearGradient><path
id="rect6741-7-4"
opacity="0.3"
fill="none"
stroke="url(#rect6741-7-4_1_)"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M4.587,3.5h22.826c0.6,0,1.087,0.487,1.087,1.087v22.826c0,0.6-0.487,1.087-1.087,1.087H4.587c-0.6,0-1.087-0.487-1.087-1.087V4.587 C3.5,3.987,3.987,3.5,4.587,3.5z" /><path
id="rect5505-6"
opacity="0.5"
fill="none"
stroke="#0E141F"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M4.66,2.5h22.68c1.193,0,2.16,0.967,2.16,2.16v22.68c0,1.193-0.967,2.16-2.16,2.16H4.66c-1.193,0-2.16-0.967-2.16-2.16V4.66 C2.5,3.467,3.467,2.5,4.66,2.5z" /><path
opacity="0.5"
fill="#273445"
d="M29.5,12.53v14.81c0,1.06-0.78,1.95-1.79,2.13H12.87l-6.9-6.72l-0.02-0.01v-0.01 c-0.01-0.01-0.01-0.01-0.01-0.01l-0.65-1.24l-0.75-1.44c-0.03-0.08,0.01-0.16,0.08-0.21c0.04-0.02,0.07-0.02,0.11-0.02 c0,0,0,0,0,0.01l0.56,0.12l0.72,0.16l0.3-0.16l0.23-0.12l3.2-1.67c0.02-0.02,0.05-0.03,0.07-0.03l1.57-0.82l-0.26-0.49 c-0.1-0.2-0.07-0.44,0.06-0.61l-0.88-0.84c-0.02-0.02-0.05-0.04-0.07-0.07c-0.04-0.05-0.08-0.1-0.11-0.16 c-0.19-0.37-0.05-0.83,0.32-1.02l4.27-2.23c0.28-0.15,0.62-0.1,0.85,0.1l0.06,0.06l2.04,1.97l1.11-0.57 c0.02-0.02,0.04-0.03,0.07-0.04l3.25-1.7l1.65-3.94c0-0.01,0-0.01,0-0.02c0.03-0.07,0.09-0.13,0.16-0.17 c0.13-0.07,0.28-0.06,0.39,0.02L29.5,12.53z"
id="path50"
style="opacity:0.15" /><path
fill="#FFFFFF"
d="M27.412,13.506l-3.038-5.82c-0.005-0.007-0.009-0.013-0.014-0.019c-0.021-0.027-0.045-0.05-0.072-0.07 c-0.108-0.079-0.256-0.095-0.383-0.029c-0.074,0.039-0.13,0.101-0.161,0.172c-0.004,0.006-0.007,0.014-0.009,0.021L22.092,11.7 l-3.258,1.701c-0.021,0.011-0.043,0.022-0.064,0.034l-1.108,0.578l-0.965,0.504l-0.252-0.484c-0.143-0.274-0.48-0.38-0.754-0.237 c-0.273,0.143-0.38,0.48-0.237,0.754l0.252,0.484l-0.932,0.487l-0.806-1.542l1.445-0.754c0.371-0.194,0.515-0.653,0.321-1.024 c-0.031-0.06-0.07-0.114-0.114-0.162l-0.066-0.064c-0.228-0.195-0.562-0.243-0.844-0.096l-4.269,2.228 c-0.372,0.194-0.516,0.653-0.322,1.024c0.031,0.058,0.067,0.111,0.11,0.157c0.022,0.025,0.046,0.049,0.072,0.069 c0.228,0.194,0.561,0.242,0.843,0.094l1.445-0.754l0.805,1.542l-1.029,0.537l-0.252-0.484c-0.143-0.273-0.481-0.379-0.754-0.237 c-0.072,0.038-0.132,0.089-0.18,0.149c-0.132,0.168-0.162,0.404-0.057,0.605l0.252,0.484l-1.567,0.818 c-0.021,0.011-0.043,0.022-0.064,0.033l-3.733,1.949l-1.274-0.276c-0.003-0.001-0.007-0.002-0.01-0.002 c-0.035-0.007-0.073-0.002-0.108,0.016c-0.076,0.04-0.109,0.13-0.078,0.208l1.397,2.675c0.003,0.005,0.007,0.01,0.011,0.015 l0.002,0.002l0.02,0.019c0.05,0.041,0.121,0.05,0.182,0.019c0.034-0.018,0.06-0.046,0.074-0.079 c0.002-0.003,0.003-0.006,0.004-0.009l0.511-1.226l1.494-0.78c0.006,0.425,0.108,0.854,0.318,1.256 c0.717,1.373,2.412,1.905,3.785,1.188c0.021-0.011,0.043-0.022,0.064-0.034l1.567-0.818l0.293,0.562 c0.143,0.273,0.481,0.379,0.754,0.237c0.273-0.143,0.38-0.48,0.237-0.754l-0.293-0.561l1.029-0.538l0.855,1.637 c0.199,0.381,0.668,0.529,1.049,0.33c0.107-0.055,0.195-0.132,0.261-0.222c0.175-0.232,0.211-0.553,0.068-0.827l-0.854-1.637 l0.932-0.487l0.293,0.561c0.143,0.273,0.48,0.38,0.754,0.237c0.274-0.143,0.379-0.481,0.237-0.754l-0.293-0.562l2.163-1.129 c0.149-0.08,0.288-0.171,0.417-0.273c1.037-0.819,1.38-2.284,0.746-3.498c-0.21-0.401-0.503-0.731-0.848-0.979l1.019-0.532 l4.194,0.909c0.008,0.002,0.015,0.004,0.022,0.005c0.078,0.015,0.159,0.005,0.234-0.035C27.406,13.871,27.477,13.676,27.412,13.506z M11.653,21.735c-0.01,0.006-0.019,0.011-0.029,0.016c-0.617,0.322-1.378,0.084-1.7-0.534c-0.322-0.617-0.084-1.378,0.533-1.7 l1.633-0.853l1.166,2.234L11.653,21.735z M14.246,20.381l-1.166-2.234l0.068-0.035l0.961-0.502l1.166,2.234l-0.219,0.114 L14.246,20.381z M16.655,19.124l-1.166-2.234l0.932-0.487l1.166,2.234L16.655,19.124z M20.523,17.102 c-0.014,0.009-0.028,0.017-0.042,0.024l-1.904,0.994l-1.166-2.234l1.42-0.742l0.455-0.237c0.024-0.012,0.048-0.024,0.073-0.035 c0.603-0.268,1.318-0.024,1.627,0.569c0.273,0.522,0.144,1.147-0.276,1.522C20.654,17.014,20.592,17.061,20.523,17.102z"
id="path52" /></svg>

Before

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -1,184 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg4405"
inkscape:version="0.91 r13725"
sodipodi:docname="48x48.svg"
x="0px"
y="0px"
width="48px"
height="48px"
viewBox="0 0 48 48"
enable-background="new 0 0 48 48"
xml:space="preserve"><metadata
id="metadata57"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs55" /><sodipodi:namedview
inkscape:zoom="8"
objecttolerance="10"
bordercolor="#666666"
borderopacity="1"
gridtolerance="10"
guidetolerance="10"
showgrid="true"
pagecolor="#ffffff"
inkscape:cx="18.590413"
inkscape:cy="19.492052"
id="namedview45"
inkscape:current-layer="g6"
inkscape:window-height="705"
inkscape:window-y="30"
inkscape:window-x="0"
inkscape:window-maximized="1"
inkscape:pageopacity="0"
inkscape:window-width="1366"
inkscape:pageshadow="2"><inkscape:grid
type="xygrid"
empspacing="4"
id="grid4182" /><inkscape:grid
type="xygrid"
color="#ff3fff"
opacity="0.31372549"
spacingy="0.5"
empopacity="0.11764706"
empcolor="#3fffff"
empspacing="2"
spacingx="0.5"
id="grid4184" /></sodipodi:namedview><g
id="g6"><g
id="g3712-0"
transform="matrix(1.1578952,0,0,0.57142859,-3.789476,19.142856)"
opacity="0.4"><radialGradient
id="rect2801-4_1_"
cx="-303.9869"
cy="6065.9893"
r="2.4994"
gradientTransform="matrix(2.3202 0 0 -0.8 743.2948 4896.2915)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop10" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop12" /></radialGradient><rect
id="rect2801-4"
x="38"
y="40"
fill="url(#rect2801-4_1_)"
width="5"
height="7" /><radialGradient
id="rect3696-8_1_"
cx="-507.3432"
cy="7800.5005"
r="2.4994"
gradientTransform="matrix(-2.3202 0 0 0.8 -1167.1165 -6196.9004)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop16" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop18" /></radialGradient><rect
id="rect3696-8"
x="5"
y="40"
fill="url(#rect3696-8_1_)"
width="5"
height="7" /><linearGradient
id="rect3700-7_1_"
gradientUnits="userSpaceOnUse"
x1="-185.5088"
y1="5712.9136"
x2="-185.5088"
y2="5719.9419"
gradientTransform="matrix(1.1579 0 0 -0.5714 238.7997 3311.5498)"><stop
offset="0"
style="stop-color:#181818;stop-opacity:0"
id="stop22" /><stop
offset="0.5"
style="stop-color:#181818"
id="stop24" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop26" /></linearGradient><rect
id="rect3700-7"
x="10"
y="40"
fill="url(#rect3700-7_1_)"
width="28"
height="7" /></g><radialGradient
id="SVGID_1_"
cx="-16.2385"
cy="25.8311"
r="122.195"
fx="-21.3515"
fy="25.8311"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#95A3AB"
id="stop30" /><stop
offset="0.2624"
style="stop-color:#9BAEC8"
id="stop32" /><stop
offset="0.705"
style="stop-color:#8093AA"
id="stop34" /><stop
offset="1"
style="stop-color:#273445"
id="stop36" /></radialGradient><path
fill="url(#SVGID_1_)"
d="M43.5,7.5v35c0,0.96-0.69,1.77-1.6,1.96c-0.13,0.03-0.26,0.04-0.4,0.04h-35c-1.1,0-2-0.9-2-2v-35 c0-1.1,0.9-2,2-2h35C42.6,5.5,43.5,6.4,43.5,7.5z"
id="path38" /><linearGradient
id="rect6741-1_1_"
gradientUnits="userSpaceOnUse"
x1="-181.34"
y1="3621.929"
x2="-181.34"
y2="3586.1492"
gradientTransform="matrix(1 0 0 -1 205.34 3629.04)"><stop
offset="0"
style="stop-color:#FFFFFF"
id="stop41" /><stop
offset="0.021"
style="stop-color:#FFFFFF;stop-opacity:0.2353"
id="stop43" /><stop
offset="0.977"
style="stop-color:#FFFFFF;stop-opacity:0.1569"
id="stop45" /><stop
offset="1"
style="stop-color:#FFFFFF;stop-opacity:0.3922"
id="stop47" /></linearGradient><path
id="rect6741-1"
opacity="0.3"
fill="none"
stroke="url(#rect6741-1_1_)"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M6.5,6.5h35c0.552,0,1,0.448,1,1v35c0,0.552-0.448,1-1,1h-35c-0.552,0-1-0.448-1-1v-35C5.5,6.948,5.948,6.5,6.5,6.5z" /><path
id="rect5505-21-6"
opacity="0.5"
fill="none"
stroke="#0E141F"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M6.5,5.5h35c1.105,0,2,0.895,2,2v35c0,1.105-0.895,2-2,2h-35c-1.105,0-2-0.895-2-2v-35C4.5,6.395,5.395,5.5,6.5,5.5z" /><path
opacity="0.5"
fill="#273445"
d="M43.5,19.234v23.477c0,0.857-0.616,1.579-1.428,1.749H19.773L9.689,34.644l-0.027-0.027 v-0.009c-0.009-0.009-0.009-0.018-0.018-0.018l-0.946-1.811l-1.098-2.097c-0.045-0.116,0.009-0.241,0.116-0.303 c0.054-0.027,0.107-0.036,0.161-0.027c0,0,0.009,0,0.009,0.009l0.812,0.178l1.053,0.223l0.437-0.232l0.33-0.17l4.685-2.445 c0.027-0.018,0.062-0.036,0.089-0.044l2.293-1.196l-0.375-0.714c-0.152-0.286-0.107-0.634,0.089-0.883l-1.285-1.232 c-0.036-0.036-0.071-0.062-0.098-0.107c-0.062-0.063-0.116-0.143-0.161-0.223c-0.285-0.544-0.08-1.214,0.464-1.499l6.237-3.257 c0.41-0.214,0.901-0.143,1.231,0.143l0.098,0.098l2.98,2.873l1.615-0.839c0.027-0.018,0.062-0.036,0.098-0.054l4.756-2.48 l2.4-5.756c0-0.009,0-0.018,0.009-0.027c0.045-0.107,0.125-0.197,0.232-0.25c0.187-0.098,0.402-0.08,0.562,0.036L43.5,19.234z"
id="path51"
style="opacity:0.15" /><path
fill="#FFFFFF"
d="M41.005,21.139l-4.436-8.499c-0.007-0.01-0.013-0.019-0.02-0.028c-0.031-0.039-0.066-0.074-0.105-0.102 c-0.158-0.115-0.373-0.139-0.559-0.042c-0.109,0.057-0.189,0.147-0.236,0.251c-0.007,0.009-0.01,0.02-0.014,0.031l-2.398,5.752 l-4.758,2.484c-0.031,0.016-0.063,0.033-0.093,0.05l-1.618,0.845l-1.41,0.736l-0.369-0.706c-0.209-0.4-0.701-0.555-1.101-0.346 c-0.399,0.208-0.555,0.701-0.346,1.101l0.369,0.706l-1.362,0.711l-1.176-2.253l2.111-1.102c0.542-0.284,0.752-0.953,0.469-1.495 c-0.045-0.088-0.102-0.167-0.167-0.236l-0.096-0.093c-0.334-0.285-0.82-0.355-1.232-0.14l-6.234,3.254 c-0.543,0.284-0.753,0.953-0.47,1.496c0.045,0.085,0.098,0.162,0.161,0.23c0.032,0.037,0.068,0.071,0.105,0.101 c0.334,0.283,0.819,0.353,1.23,0.138l2.11-1.101l1.176,2.253l-1.503,0.785l-0.369-0.706c-0.209-0.399-0.703-0.554-1.101-0.346 c-0.105,0.055-0.193,0.129-0.263,0.217c-0.193,0.245-0.237,0.589-0.083,0.884l0.369,0.706l-2.289,1.195 c-0.031,0.015-0.063,0.032-0.094,0.048L9.75,30.763l-1.86-0.404c-0.005-0.002-0.01-0.002-0.014-0.003 c-0.052-0.01-0.107-0.002-0.157,0.023c-0.111,0.058-0.159,0.189-0.115,0.303l2.04,3.907c0.005,0.007,0.01,0.015,0.016,0.021 l0.003,0.003l0.03,0.028c0.072,0.06,0.176,0.073,0.266,0.027c0.05-0.027,0.087-0.067,0.109-0.116 c0.002-0.004,0.004-0.008,0.006-0.013l0.747-1.79l2.182-1.14c0.009,0.62,0.158,1.247,0.464,1.834 c1.047,2.005,3.522,2.782,5.528,1.735c0.031-0.016,0.063-0.033,0.093-0.05l2.289-1.195l0.428,0.82 c0.208,0.399,0.703,0.554,1.101,0.346c0.399-0.208,0.555-0.702,0.346-1.101l-0.428-0.819l1.503-0.785l1.248,2.391 c0.291,0.557,0.976,0.772,1.533,0.481c0.156-0.081,0.285-0.193,0.382-0.324c0.255-0.339,0.308-0.807,0.099-1.208l-1.248-2.391 l1.362-0.711l0.428,0.819c0.208,0.399,0.702,0.555,1.101,0.346c0.4-0.209,0.554-0.703,0.346-1.101l-0.428-0.82l3.158-1.649 c0.218-0.117,0.421-0.25,0.609-0.399c1.515-1.196,2.015-3.336,1.09-5.108c-0.306-0.586-0.735-1.067-1.238-1.43l1.489-0.777 l6.125,1.327c0.012,0.003,0.022,0.006,0.033,0.007c0.113,0.021,0.233,0.007,0.341-0.05C40.997,21.673,41.1,21.388,41.005,21.139z M17.991,33.156c-0.014,0.009-0.027,0.015-0.042,0.023c-0.902,0.471-2.013,0.122-2.483-0.78c-0.47-0.901-0.122-2.012,0.778-2.482 l2.385-1.245l1.703,3.262L17.991,33.156z M21.778,31.179l-1.703-3.262l0.1-0.051l1.404-0.733l1.703,3.262l-0.32,0.167 L21.778,31.179z M25.296,29.343l-1.703-3.262l1.362-0.711l1.703,3.262L25.296,29.343z M30.945,26.39 c-0.02,0.013-0.04,0.025-0.061,0.035l-2.78,1.452l-1.703-3.262l2.073-1.083l0.664-0.346c0.035-0.018,0.071-0.035,0.107-0.052 c0.88-0.392,1.924-0.035,2.376,0.831c0.399,0.762,0.21,1.675-0.403,2.223C31.136,26.263,31.045,26.331,30.945,26.39z"
id="path53" /></g></svg>

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -1,184 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg4332"
inkscape:version="0.91 r13725"
sodipodi:docname="64x64.svg"
x="0px"
y="0px"
width="64px"
height="64px"
viewBox="0 0 64 64"
enable-background="new 0 0 64 64"
xml:space="preserve"><metadata
id="metadata54"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs52" /><sodipodi:namedview
inkscape:zoom="8"
objecttolerance="10"
bordercolor="#666666"
borderopacity="1"
gridtolerance="10"
guidetolerance="10"
showgrid="true"
pagecolor="#ffffff"
inkscape:cx="16.97465"
inkscape:cy="32.785763"
id="namedview46"
inkscape:current-layer="svg4332"
inkscape:window-height="705"
inkscape:window-y="30"
inkscape:window-x="0"
inkscape:window-maximized="1"
inkscape:pageopacity="0"
inkscape:window-width="1366"
inkscape:pageshadow="2"><inkscape:grid
type="xygrid"
empspacing="4"
id="grid4203" /><inkscape:grid
type="xygrid"
color="#ff3fff"
opacity="0.29803922"
spacingy="0.5"
empopacity="0.1372549"
empcolor="#3fffff"
empspacing="2"
spacingx="0.5"
id="grid4187" /></sodipodi:namedview><g
id="g3712"
transform="matrix(1.5789502,0,0,0.7142857,-5.8947511,28.428574)"
opacity="0.4"><radialGradient
id="rect2801_1_"
cx="-238.0086"
cy="5000.9541"
r="2.4994"
gradientTransform="matrix(3.1639 0 0 -1 791.0224 5044.4541)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop8" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop10" /></radialGradient><rect
id="rect2801"
x="38"
y="40"
fill="url(#rect2801_1_)"
width="5"
height="7" /><radialGradient
id="rect3696_1_"
cx="-344.166"
cy="6088.6416"
r="2.4994"
gradientTransform="matrix(-3.1639 0 0 1 -1078.8911 -6045.1416)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#181818"
id="stop14" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop16" /></radialGradient><rect
id="rect3696"
x="5"
y="40"
fill="url(#rect3696_1_)"
width="5"
height="7" /><linearGradient
id="rect3700_1_"
gradientUnits="userSpaceOnUse"
x1="-172.6913"
y1="4778.4775"
x2="-172.6913"
y2="4785.5059"
gradientTransform="matrix(1.579 0 0 -0.7143 296.6709 3460.2258)"><stop
offset="0"
style="stop-color:#181818;stop-opacity:0"
id="stop20" /><stop
offset="0.5"
style="stop-color:#181818"
id="stop22" /><stop
offset="1"
style="stop-color:#181818;stop-opacity:0"
id="stop24" /></linearGradient><rect
id="rect3700"
x="10"
y="40"
fill="url(#rect3700_1_)"
width="28"
height="7" /></g><radialGradient
id="rect5505-21-3-8-5-2-9_1_"
cx="-434.767"
cy="4038.6685"
r="12.6719"
fx="-435.2973"
fy="4038.6685"
gradientTransform="matrix(0 13.394 16.4435 0 -66376.8047 5798.5156)"
gradientUnits="userSpaceOnUse"><stop
offset="0"
style="stop-color:#9BAEC8"
id="stop28" /><stop
offset="0.2624"
style="stop-color:#9BAEC8"
id="stop30" /><stop
offset="0.705"
style="stop-color:#8093AA"
id="stop32" /><stop
offset="1"
style="stop-color:#273445"
id="stop34" /></radialGradient><path
id="rect5505-21-3-8-5-2-9"
fill="url(#rect5505-21-3-8-5-2-9_1_)"
d="M7.5,4.5h49c1.657,0,3,1.343,3,3v49c0,1.657-1.343,3-3,3 h-49c-1.657,0-3-1.343-3-3v-49C4.5,5.843,5.843,4.5,7.5,4.5z" /><linearGradient
id="rect6741_1_"
gradientUnits="userSpaceOnUse"
x1="-261.3883"
y1="3756.1287"
x2="-261.3883"
y2="3719.9463"
gradientTransform="matrix(1.4324 0 0 -1.4363 406.4211 5400.8813)"><stop
offset="0"
style="stop-color:#FFFFFF"
id="stop38" /><stop
offset="0.0349"
style="stop-color:#FFFFFF;stop-opacity:0.2353"
id="stop40" /><stop
offset="0.9622"
style="stop-color:#FFFFFF;stop-opacity:0.1569"
id="stop42" /><stop
offset="1"
style="stop-color:#FFFFFF;stop-opacity:0.3922"
id="stop44" /></linearGradient><path
id="rect6741"
opacity="0.3"
fill="none"
stroke="url(#rect6741_1_)"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M7.5,5.429h49c1.105,0,2,0.895,2,2v49.142c0,1.105-0.895,2-2,2h-49c-1.105,0-2-0.895-2-2V7.429C5.5,6.324,6.395,5.429,7.5,5.429z" /><path
id="rect5505-21-9"
opacity="0.5"
fill="none"
stroke="#0E141F"
stroke-linecap="round"
stroke-linejoin="round"
enable-background="new "
d=" M7.5,4.5h49c1.657,0,3,1.343,3,3v49c0,1.657-1.343,3-3,3h-49c-1.657,0-3-1.343-3-3v-49C4.5,5.843,5.843,4.5,7.5,4.5z" /><path
opacity="0.5"
fill="#273445"
d="M59.5,23.606v32.74c0,1.77-1.436,3.205-3.205,3.205H25.493L11.094,45.523l-0.042-0.042v-0.005 c-0.011-0.011-0.016-0.021-0.026-0.027l-2.914-5.584c-0.064-0.164,0.005-0.35,0.164-0.434c0.074-0.037,0.154-0.048,0.222-0.032 c0.011,0,0.016,0,0.021,0.005l2.659,0.578l0.625-0.329l0.472-0.244l6.691-3.497c0.042-0.021,0.09-0.048,0.132-0.069l3.274-1.706 l-0.53-1.012c-0.217-0.419-0.154-0.911,0.122-1.261l-1.833-1.77c-0.053-0.042-0.101-0.09-0.148-0.143 c-0.09-0.101-0.164-0.207-0.228-0.328c-0.408-0.779-0.106-1.732,0.673-2.14l8.906-4.646c0.588-0.307,1.282-0.207,1.759,0.201 l0.138,0.132l4.259,4.116l2.31-1.203c0.042-0.027,0.09-0.048,0.132-0.074l6.797-3.55l3.428-8.217 c0.005-0.016,0.011-0.032,0.021-0.042c0.064-0.148,0.18-0.275,0.334-0.36c0.265-0.138,0.572-0.106,0.8,0.058L59.5,23.606z"
id="path48"
style="opacity:0.15" /><path
fill="#FFFFFF"
d="M55.829,26.231l-6.338-12.142c-0.01-0.014-0.018-0.027-0.029-0.04c-0.044-0.056-0.094-0.105-0.151-0.146 c-0.226-0.165-0.534-0.199-0.798-0.06c-0.155,0.082-0.271,0.21-0.337,0.359c-0.009,0.013-0.014,0.029-0.019,0.044l-3.426,8.217 l-6.798,3.549c-0.045,0.023-0.091,0.047-0.133,0.071l-2.311,1.207l-2.014,1.051l-0.527-1.009c-0.299-0.572-1.001-0.793-1.573-0.494 c-0.57,0.298-0.793,1.001-0.494,1.573l0.527,1.009l-1.946,1.016l-1.681-3.218l3.016-1.574c0.774-0.405,1.075-1.362,0.671-2.136 c-0.065-0.126-0.145-0.238-0.238-0.338l-0.137-0.133c-0.477-0.407-1.172-0.507-1.76-0.2l-8.907,4.649 c-0.775,0.405-1.076,1.362-0.672,2.137c0.064,0.121,0.14,0.231,0.23,0.328c0.046,0.053,0.097,0.102,0.149,0.145 c0.476,0.405,1.17,0.504,1.758,0.196l3.015-1.573l1.679,3.218l-2.148,1.121l-0.527-1.009c-0.299-0.57-1.004-0.791-1.573-0.494 c-0.151,0.078-0.276,0.185-0.376,0.31c-0.275,0.35-0.338,0.842-0.119,1.263l0.527,1.009l-3.27,1.708 c-0.045,0.022-0.089,0.045-0.134,0.069l-7.788,4.066L8.52,39.402C8.513,39.4,8.506,39.399,8.5,39.398 c-0.074-0.014-0.153-0.003-0.225,0.033c-0.159,0.083-0.227,0.271-0.164,0.433l2.914,5.581c0.007,0.011,0.015,0.021,0.023,0.03 l0.004,0.005l0.042,0.04c0.103,0.086,0.252,0.105,0.38,0.039c0.072-0.038,0.124-0.096,0.155-0.165 c0.003-0.006,0.006-0.012,0.008-0.019l1.067-2.558l3.117-1.628c0.012,0.886,0.226,1.782,0.664,2.62 c1.496,2.864,5.032,3.975,7.897,2.479c0.045-0.023,0.089-0.047,0.133-0.071l3.27-1.708l0.611,1.172 c0.298,0.57,1.004,0.791,1.573,0.494c0.57-0.298,0.793-1.002,0.495-1.573l-0.611-1.17l2.148-1.122l1.784,3.416 c0.415,0.795,1.394,1.103,2.189,0.688c0.222-0.115,0.407-0.276,0.545-0.463c0.365-0.484,0.44-1.153,0.141-1.726l-1.782-3.416 l1.946-1.016l0.611,1.17c0.298,0.57,1.002,0.793,1.573,0.495c0.572-0.299,0.791-1.004,0.494-1.573l-0.611-1.172l4.512-2.356 c0.312-0.166,0.602-0.358,0.87-0.57c2.164-1.708,2.879-4.766,1.557-7.298c-0.437-0.837-1.05-1.525-1.769-2.042l2.127-1.11 l8.751,1.897c0.017,0.004,0.031,0.009,0.046,0.01c0.162,0.031,0.332,0.01,0.488-0.072C55.818,26.993,55.966,26.586,55.829,26.231z M22.951,43.399c-0.02,0.012-0.039,0.022-0.06,0.033c-1.288,0.673-2.875,0.174-3.548-1.114c-0.671-1.287-0.174-2.874,1.111-3.547 l3.408-1.779l2.433,4.66L22.951,43.399z M28.361,40.574l-2.433-4.66l0.142-0.074l2.006-1.047l2.433,4.66l-0.458,0.239L28.361,40.574 z M33.386,37.952l-2.433-4.66l1.946-1.016l2.433,4.66L33.386,37.952z M41.458,33.733c-0.028,0.018-0.058,0.035-0.087,0.05 l-3.972,2.074l-2.433-4.66l2.962-1.547l0.949-0.495c0.051-0.026,0.101-0.05,0.153-0.074c1.258-0.559,2.749-0.05,3.395,1.187 c0.57,1.089,0.3,2.393-0.575,3.176C41.73,33.55,41.6,33.648,41.458,33.733z"
id="path50" /></svg>

Before

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -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.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 80 KiB

233
data/icons/color.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

24
data/icons/symbolic.svg Normal file
View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
<defs>
<radialGradient id="radial0" gradientUnits="userSpaceOnUse" cx="26.10651" cy="259.029419" fx="26.10651" fy="259.029419" r="16.961348" gradientTransform="matrix(1.293566,0.0000000199752,-0.0000000199752,1.293566,-279.770477,-257.071594)">
<stop offset="0" style="stop-color:rgb(96.078432%,76.078433%,6.666667%);stop-opacity:1;"/>
<stop offset="0.4572" style="stop-color:rgb(96.078432%,76.078433%,6.666667%);stop-opacity:1;"/>
<stop offset="0.685319" style="stop-color:rgb(97.647059%,85.882354%,57.647061%);stop-opacity:1;"/>
<stop offset="0.822382" style="stop-color:rgb(96.078432%,74.901962%,17.647059%);stop-opacity:1;"/>
<stop offset="1" style="stop-color:rgb(89.803922%,64.705884%,3.921569%);stop-opacity:1;"/>
<stop offset="1" style="stop-color:rgb(89.803922%,64.705884%,3.921569%);stop-opacity:1;"/>
</radialGradient>
<image id="image1393" width="192" height="152" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAMNElEQVR4nO3dy3MbV3bH8e+53Q2A4Et8gHrLiETJNlWWa8IxPXEqJWaTVSo75d+R/G94l8rO3E5VtvQqKy1SiZUal8pRObIVh7YlkiJBAN3nlwUIiXIcj+OhSIo4nwWLJYGNbvKc+74XEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghHBI77hsIx0/ajwMDAx3z7RypSIARJmF8jHF7NT3Y2EhLfF5xBzcbnSSIBBhREsYa6VGrXUyXqZ5VvWxri84lHvdGKQnScd9AOHpiEPxPxi7UW5VNF93qQqbiUpHXJ2m1C0aoYIwEGDEC49NB8Bf17EzVVbuX7FYfv5YKTX+3VS9YuxMJEE4pYbTaxVSqTdXK7IqSvW/i/UR2vkDNfKqb0doYmQTIj/sGwtHRPRL/tFg8h8kKv4TpFuK3mE0JtsxSzk7f7n+9PTIJEDXAiJAwlm7mX+91JspUXRC8h/ShxDvJmJNUYDKmYfnG5Eh0gCESYCRIGPeXc1o7zelGdjbJlgQfgt0EzkmMDV5oYvN47/WoRRPolJOw+58s51dnnjbz8bTgpHcr6Xdm3BJclCkls+SiIpVON41M6Q9RA5xqEsb6arZ883kjTaR5TG87+tDgN4auAFOGFQgSKknJNZG0zvpx3/qRiRrglBoGP2w0Nnf25o38RmWsGLYMamM2bSKX6DmSkUpV+IwnrT4djUkwiBrgVBoEPxlsNJ7t7cxTpeuYVgw+wHQVmDGoa/j3N3OSl0WW/IfjvfUjFzXAKTMM/g1ajWJ3d85SuuboA5wPk3EdmAMacpIZpQwhXJ5K6LtXjZEp/SFqgFNlf3FbtrHRauTl5KxZuubOimG/M+OGjHlgDMiwF8sdZIaTqVSWXFN5JEB487wo+ZdajWJ8Yi71bRHTCqa/AN4GtWwY/AfW+gyWP8tNViozb/W+E3dGpw8QTaBT4GWbfxD8lmwR9IFLHxm8A1oAxiRyDgS/2E8AmZOpr7J0qmxkgh+iBnjj3btH4pPl/GFnsbnZa7YMbrj0oYbBbywATXg1+IdkSIarsoos+ROAj4/4IY5R1ABvMN0jPVi6mT+deTo2XfVapnzRTSsMZnlvCBZsMMv7k8FvIBcgPCX1VSavOjERFk44gUmkhyuLxRSbE3mpc4WKJZf9pWEfGfYOsGD2fwf/K5czuUhlPU9etnKNUAUQCfCmkQbr+Vlv1yZgany8dtHhlsRfIX0keBusZcYY+vng1+CCAqvAq05landy3b0bneBwAg07u9/una1nXU2NyS9KftPdVixxC+wtiVmDuvTqaM9PGWx7NBdWyagaWdcflnUWj+h5ToJIgDfAcPP6g7Wb+bmJbqM+V80Irrh4T8lWMmNJ4hIwbVAHJf74tka5wMANlfLku12geP3Pc5JEApxww83r3G4Xl58+G+9abd7wq8mzW0r6rcHbEucEkzYI318S/PsXRyRcUplZ5VU9afFyfWSaPxAJcGLtn9Vj3F/OaH1V3+3Xp7q17HyN6npl6c9dumVwFZg3aCIKEoZ+cfAPmkAyR5QSLhCfA0uv88lOlkiAE0Zg3MNYX02PNx4W07P/1dzz+mytLC9nyZdc/AZ41+AiMIPRkMgw0q/oug5mgc36GN4sEDuH/kgnWiTACTFs56/fJrUftfPp8qt6c7o+5V2dzcyvgb9n8B7YVYMFYAKshvRHO7s/xQYTYDLhoFKVu1KuB4xUBRAJcNxeBP46aXVlMbvVoe6z3Ql6ac6SLgveQbwns+ugi8CZ/TU9uX5ZZ/en33fwRTBIAJL5zg4sXR2t1aCRAMdAL9vpdv+T5Wz59vfZ+xupvjvVb9ZcM708vwhcM2nJEjdMdgU0J5gwqAkS9v9o7/+I2WD0H0BmDpQyXA0bqeCHSIAjITh40KCxvpoedh5nk9l2fv2tb+udTqNZTOtMX9m5Cv1ZgndIuo64LFnL0KRB3SDXcJTnTwhVHfxWckwlnvl4NVrLICAS4LUYBvzHH2N37wJrGPeXE/+9mb7NtvN67VExLRq1vDbe7xWzlfxs5lyR/Lph1zBdRDYPTAk1DMtBSYc4c2+YNKiLXFgpK52xPd2/P8ny8mG9y8kXCXDIXozbf7Kc7q5sJn6/mzHRzLef7RTdvFtPXjRrZT5Zyeesm84m1E6JtuCyZOcwZsEmgAZQGKTBeSWHe17nsAWE8GQqzc1VRhMo/Anu3SOxvpqejH1RG5/6ttHrWqMosjGXN/N+OZEpnbGU5vuVX8gyXXbpohkLYLOCKTOaQJ1XJ7Re0yltGuwFMFyy0pL5FqN1KBZEAhya/XU6id4P9cKz6Ur5fFGzlsGC4WcdzprsnCpaJJ+BNA1MIpoaBH2NwW6t1xz4HCj+4eUwaPJJkqi/tnc9kSIBDo9BO98otyZTPb9k0rsuezfBW5AWJM6Y2QTQRNYA1RiU9DkHg/7FTt2f6eYa/NoRoB8RDPoArxyKNTIng8Zy6MMkJh9VrVrq0NeWe3qeBn3NGaHzQhdknJWYA6YY7NKqAZm9LPGFD9rlQIVRAqVB36APlEC1/ypnEMD6NR9msf8zwvBRPRQLogY4NGZI96hYSXv9LH3foP+lzCYkO5PQlGTjDDq2w8/h8mHH9uWQPC72g19UQLX/usFLRMJIGBmDv12OkaRXmk2/qPzenweQhKPBoVgasUOxIBLgcN1Da58+LO+02tvPe/nXlUqDDHcvzawjdB5jUoOSfxi0g1IcKokS6IHtYewJdQ16mDmSychN1ARjhprCxmy//6DBiNGwD5F+STPJGOwHRl4WWe49f/HvIyMS4BAZSH+Pr336qP9Bq70197z+VVmrerm06fA16BrY/jqewRCnwDUo7XsYuxjbuJ4BT5OxCbaDVLpSslTVhU2aMYtbC9O8ZDMJpgWTwJigkSBHlh1YKvGTMb2/HMLNUr9b9l0vD8UamVogEuCQDZMAPerx4ObWsy96ZVGz7S72WOYt3OayzKYcGrgysCpL7LnbruHPXb5jZrtVZZ2Us+dV1VeW3KrK6lmeVdavJ2XNUkxbSi3JL7rsspkuSCwYzGJMyjQmDWaP95tOdrBWsEErSPu1S1nLku/tjtahWBAJ8Frsj69L9z4vz9xeff7k+Re98SJt15N90y99rJTV68qzvpOU8FJllWdFzyv6Ven9qqqVY81uWWW1Sr2GazeXWtDrdFMas7TTSUWhVCuystnr23RhmpXpvIk2ZleFLuG2YGZnQOPY/jKK4TZJcWAkVC5ZqQr36e/E34xO6Q+j1dw7FsONLfc/Wc6WL28mqt3sO5/M5ovKnhZu6id5P1PVeOb9zcL3Jhq+uDDtbE9q/bN1VpfQGnBneMHWqjG5bY/+9fusfaGTbfbrtSK3RpmyiXLP51LOuURqu2kRtzaJc7hmMSaAhqAwkQSVGU8N/s2Mf5TKf56uqif87Td7Zvhx/b6OWtQAr9lwuFG6PyxZbX4Nowv/0sJejLr8HeJj4MCJDKt//b8+ud3EOgj+4fdUS+PYndZql87j3Y2tze3G7NwPhXrfVJX+g1L/rsQVly2mRNvFBaQ5M5s0o44wwzpm2pPTU8oq5qRROhQLIgGOzIGx+v2+J7Y6/L+DQX7vZy+jYQP+xXW0bkDVWqPkzJUuXz7ffVbkW7XxaqPTa3yVWfkH0PkEbTeuAhdcNmvmDWEduf2gjB15r3z0h0Ltw3rgN0Q0gU6R4eYabq8mOo+z7/pZrarKMav1J+q5nalkrQTnjHQeaU5mmaEnZnxm/e6X/9mZ2Vq683l/VD4lHiIBTqUXibCEPeBmNjP2NG9aXu9XGlMqxou6j3tPTcuylMpqN/f09fiUNll91Bul9j9EApxqBzbiGOuDWuHbbDuv744VnbyWW+Y25r3y6XjqtDce9bmDj1LpD5EAI2O4DXNtjXSnhfHFsrEMbE+Kz9adu79uTdGbLhJgBP1oiyajGPghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhPDm+x+8b2xxUlIm4AAAAABJRU5ErkJggg=="/>
<image id="image1419" width="192" height="152" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAAiElEQVR4nO3BMQEAAADCoPVP7WsIoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAPIpwABWLk4FQAAAABJRU5ErkJggg=="/>
</defs>
<g id="surface1370">
<path style="fill:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:miter;stroke:url(#radial0);stroke-miterlimit:4;" d="M -230 78 C -230 86.835938 -237.164062 94 -246 94 " transform="matrix(0.000000000000000061,-1,1,0.000000000000000061,-160,-172)"/>
<use xlink:href="#image1393" transform="matrix(1,0,0,1,-168,-16)"/>
<use xlink:href="#image1419" transform="matrix(1,0,0,1,-168,-16)"/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,12.156863%,19.215687%);fill-opacity:1;" d="M 14.476562 1 C 13.429688 3.382812 10.09375 5 8 5 L 8 7 C 10.09375 7 13.429688 8.617188 14.476562 11 L 15 11 L 15 1 Z M 14.476562 1 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,12.156863%,19.215687%);fill-opacity:1;" d="M 2 5 L 1.996094 7 L 8.257812 7.011719 L 8.265625 5.011719 Z M 2 5 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,12.156863%,19.215687%);fill-opacity:1;" d="M 7.5 5 C 5.023438 5 3 7.023438 3 9.5 C 3 11.976562 5.023438 14 7.5 14 C 9.976562 14 12 11.976562 12 9.5 C 12 7.023438 9.976562 5 7.5 5 Z M 7.5 7 C 8.902344 7 10 8.097656 10 9.5 C 10 10.902344 8.902344 12 7.5 12 C 6.097656 12 5 10.902344 5 9.5 C 5 8.097656 6.097656 7 7.5 7 Z M 7.5 7 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.117648%,12.156863%,19.215687%);fill-opacity:1;" d="M 0 4 L 1 5 L 1 7 L 0 8 Z M 0 4 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -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',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

255
data/ui/dialogs/compose.ui Normal file
View File

@ -0,0 +1,255 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">dialog-warning-symbolic</property>
</object>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">mail-attachment-symbolic</property>
</object>
<object class="GtkImage" id="image4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">face-smile-symbolic</property>
</object>
<template class="TootleDialogsCompose" parent="GtkWindow">
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="type_hint">dialog</property>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="has_subtitle">False</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuButton" id="visibility_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImage" id="visibility_icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">pan-down-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="post_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="horizontal"/>
<class name="linked"/>
</style>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="spinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="box">
<property name="width_request">500</property>
<property name="height_request">250</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_bottom">8</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">8</property>
<child>
<object class="GtkRevealer" id="cw_revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkEntry" id="cw">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">8</property>
<property name="placeholder_text" translatable="yes">Write your warning here</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="width_request">350</property>
<property name="height_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="overlay_scrolling">False</property>
<child>
<object class="GtkTextView" id="content">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="pixels_below_lines">8</property>
<property name="pixels_inside_wrap">8</property>
<property name="wrap_mode">word-char</property>
<property name="left_margin">8</property>
<property name="right_margin">8</property>
<property name="top_margin">8</property>
<property name="bottom_margin">8</property>
<property name="accepts_tab">False</property>
<property name="populate_all">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="emoji_button">
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image">image4</property>
<property name="relief">none</property>
<property name="always_show_image">True</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="attach_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image">image2</property>
<property name="relief">none</property>
<property name="always_show_image">True</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="cw_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image">image1</property>
<property name="relief">none</property>
<property name="always_show_image">True</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="counter">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">250</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</template>
</interface>

82
data/ui/dialogs/main.ui Normal file
View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="TootleDialogsMainWindow" parent="GtkWindow">
<property name="width_request">450</property>
<property name="height_request">600</property>
<property name="can_focus">False</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkButton" id="back_button">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="compose_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-edit-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="title">
<object class="GraniteWidgetsModeButton" id="timeline_switcher">
<property name="visible">True</property>
</object>
</child>
<child>
<object class="TootleWidgetsAccountsButton" id="accounts_button">
<property name="visible">True</property>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="view_stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">slide-left-right</property>
<child>
<object class="GtkStack" id="timeline_stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">slide-left-right</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">0</property>
<property name="title" translatable="yes">0</property>
</packing>
</child>
</object>
</child>
</template>
</interface>

155
data/ui/views/base.ui Normal file
View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="TootleViewsBase" parent="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="scrolled">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkBox" id="view">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkStack" id="states">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="vhomogeneous">False</property>
<property name="transition_type">crossfade</property>
<child>
<object class="GtkBox" id="status">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_top">16</property>
<property name="margin_bottom">16</property>
<property name="orientation">vertical</property>
<property name="spacing">16</property>
<child>
<object class="GtkImage">
<property name="width_request">128</property>
<property name="height_request">128</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="opacity">0.29999999999999999</property>
<property name="pixel_size">128</property>
<property name="icon_name">com.github.bleakgrey.tootle-symbolic</property>
<property name="icon_size">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="status_stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="opacity">0.5</property>
<property name="transition_type">crossfade</property>
<child>
<object class="GtkLabel" id="status_message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="name">message</property>
<property name="title" translatable="yes">page0</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="status_spinner">
<property name="height_request">32</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">spinner</property>
<property name="title" translatable="yes">page1</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="status_button">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="no_show_all">True</property>
<property name="halign">center</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">status</property>
<property name="title" translatable="yes">page1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="content">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="title" translatable="yes">page0</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</template>
</interface>

View File

@ -0,0 +1,224 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkGrid" id="wizard">
<property name="width_request">350</property>
<property name="height_request">400</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_top">12</property>
<property name="margin_bottom">12</property>
<property name="orientation">vertical</property>
<property name="row_spacing">12</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">12</property>
<property name="margin_right">12</property>
<property name="hexpand">True</property>
<child>
<object class="GtkButton" id="next">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_default">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="icon_name">go-next-symbolic</property>
</object>
</child>
<style>
<class name="image-button"/>
<class name="circular"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="transition_type">slide-left-right</property>
<child>
<object class="GtkGrid" id="step1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">12</property>
<property name="margin_right">12</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="row_spacing">6</property>
<property name="column_spacing">6</property>
<child>
<object class="GtkLabel" id="step1_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Which Instance?
</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<style>
<class name="h2"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="valign">start</property>
<property name="label" translatable="yes">&lt;a href="https://joinmastodon.org/"&gt;What's an instance?&lt;/a&gt;</property>
<property name="use_markup">True</property>
<property name="justify">right</property>
<property name="track_visited_links">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="instance_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="activates_default">True</property>
<property name="caps_lock_warning">False</property>
<property name="placeholder_text" translatable="yes">instance.domain</property>
<property name="input_purpose">url</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child>
<object class="GtkGrid" id="step2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">12</property>
<property name="margin_right">12</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="row_spacing">6</property>
<property name="column_spacing">6</property>
<child>
<object class="GtkLabel" id="step2_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Grant Account Access
</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<style>
<class name="h2"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="code_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="secondary_icon_name">edit-paste-symbolic</property>
<property name="secondary_icon_tooltip_text" translatable="yes">Paste</property>
<property name="placeholder_text" translatable="yes">Paste your authorization code here</property>
<property name="input_purpose">url</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="reset">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">end</property>
<property name="valign">start</property>
<property name="label" translatable="yes">&lt;a href=""&gt;Try another instance?&lt;/a&gt;</property>
<property name="use_markup">True</property>
<property name="justify">right</property>
<property name="track_visited_links">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">page1</property>
<property name="title" translatable="yes">page1</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="pixel_size">128</property>
<property name="icon_name">com.github.bleakgrey.tootle</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
</interface>

View File

@ -0,0 +1,525 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkPopover" id="filter_popover">
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="orientation">vertical</property>
<property name="spacing">8</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkRadioButton" id="filter_all">
<property name="label" translatable="yes">Posts</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="filter_replies">
<property name="label" translatable="yes">Posts and Replies</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">filter_all</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="filter_media">
<property name="label" translatable="yes">Media</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">filter_all</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkMenu" id="options">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuItem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">__glade_unnamed_3</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">__glade_unnamed_8</property>
<property name="use_underline">True</property>
</object>
</child>
<child>
<object class="GtkMenuItem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">__glade_unnamed_9</property>
<property name="use_underline">True</property>
</object>
</child>
</object>
<object class="GtkGrid" id="grid">
<property name="width_request">400</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="row_spacing">8</property>
<property name="column_spacing">8</property>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
<property name="activate_on_single_click">False</property>
<child>
<object class="GtkListBoxRow">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="TootleWidgetsRichLabel" id="note">
<property name="visible">True</property>
<property name="wrap">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="selectable">True</property>
<property name="width_chars">25</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
<property name="activate_on_single_click">False</property>
<child>
<object class="GtkListBoxRow">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="spacing">8</property>
<child>
<object class="GtkBox">
<property name="height_request">40</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="spacing">32</property>
<child>
<object class="GtkRadioButton" id="posts_tab">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<child>
<object class="GtkLabel" id="posts_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">0 Posts</property>
<property name="use_markup">True</property>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="following_tab">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">posts_tab</property>
<child>
<object class="GtkLabel" id="following_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">0 Follows</property>
<property name="use_markup">True</property>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="followers_tab">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">posts_tab</property>
<child>
<object class="GtkLabel" id="followers_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">0 Followers</property>
<property name="use_markup">True</property>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<style>
<class name="linked"/>
<class name="horizontal"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="filter_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<property name="draw_indicator">True</property>
<property name="popover">filter_popover</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">view-more-symbolic</property>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
<property name="activate_on_single_click">False</property>
<child>
<object class="GtkListBoxRow">
<property name="width_request">100</property>
<property name="height_request">80</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="spacing">8</property>
<child>
<object class="TootleWidgetsAvatar" id="avatar">
<property name="visible">True</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="size">128</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="orientation">vertical</property>
<property name="spacing">8</property>
<child>
<object class="TootleWidgetsRichLabel" id="name">
<property name="visible">True</property>
<property name="selectable">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="TootleWidgetsRichLabel" id="handle">
<property name="visible">True</property>
<property name="selectable">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="spacing">8</property>
<child>
<object class="GtkLabel" id="relationship">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="actions">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButton" id="follow_button">
<property name="label" translatable="yes">Follow</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="options_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="draw_indicator">True</property>
<property name="popup">options</property>
<property name="use_popover">False</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="horizontal"/>
<class name="linked"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
</interface>

View File

@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkPopover" id="popover">
<property name="can_focus">False</property>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hhomogeneous">False</property>
<property name="vhomogeneous">False</property>
<property name="transition_type">slide-left-right</property>
<property name="interpolate_size">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">4</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkModelButton" id="item_accounts">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes"> </property>
<property name="use_markup">True</property>
<property name="menu_name">accounts</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="item_favs">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Favorites</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="item_direct">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Conversations</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="item_watchlist">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Watchlist</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="item_refresh">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Refresh</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="item_search">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Search</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="item_prefs">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Preferences</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">9</property>
</packing>
</child>
</object>
<packing>
<property name="name">menu</property>
<property name="title" translatable="yes">page0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="width_request">400</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">4</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Accounts</property>
<property name="menu_name">menu</property>
<property name="inverted">True</property>
<property name="centered">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<property name="max_content_height">300</property>
<property name="propagate_natural_height">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="account_list">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">accounts</property>
<property name="title" translatable="yes">page1</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<template class="TootleWidgetsAccountsButton" parent="GtkMenuButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="popover">popover</property>
<child>
<object class="GtkOverlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="TootleWidgetsAvatar" id="avatar">
<property name="visible">True</property>
<property name="size">24</property>
</object>
<packing>
<property name="pass_through">True</property>
<property name="index">-1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkSpinner" id="spinner">
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="active">True</property>
</object>
<packing>
<property name="pass_through">True</property>
<property name="index">-1</property>
</packing>
</child>
</object>
</child>
</template>
</interface>

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="TootleWidgetsAccountsButtonItem" parent="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="row_spacing">3</property>
<property name="column_spacing">8</property>
<child>
<object class="TootleWidgetsAvatar" id="avatar">
<property name="visible">True</property>
<property name="size">48</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="height">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="valign">center</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-delete-symbolic</property>
</object>
</child>
<style>
<class name="circular"/>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
<property name="height">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="name">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Name</property>
<property name="ellipsize">end</property>
<property name="single_line_mode">True</property>
<property name="xalign">0</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="handle">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Handle</property>
<property name="ellipsize">end</property>
<property name="single_line_mode">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="profile">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Open profile</property>
<property name="valign">center</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">avatar-default-symbolic</property>
</object>
</child>
<style>
<class name="circular"/>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
<property name="height">2</property>
</packing>
</child>
</template>
</interface>

293
data/ui/widgets/status.ui Normal file
View File

@ -0,0 +1,293 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">emblem-favorite-symbolic</property>
</object>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">mail-replied-symbolic</property>
</object>
<object class="GtkImage" id="reblog_icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">media-playlist-repeat-symbolic</property>
</object>
<template class="TootleWidgetsStatus" parent="GtkEventBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">start</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSeparator" id="separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="column_spacing">8</property>
<child>
<object class="GtkImage" id="header_icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="valign">start</property>
<property name="margin_bottom">8</property>
<property name="icon_name">applications-development-symbolic</property>
<property name="icon_size">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="actions">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">8</property>
<property name="spacing">8</property>
<child>
<object class="GtkToggleButton" id="reblog_button">
<property name="label" translatable="yes">0</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">reblog_icon</property>
<property name="always_show_image">True</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="favorite_button">
<property name="label" translatable="yes">0</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">image2</property>
<property name="always_show_image">True</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="reply_button">
<property name="label" translatable="yes">0</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image">image3</property>
<property name="always_show_image">True</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
<property name="width">3</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="spacing">8</property>
<child>
<object class="TootleWidgetsRichLabel" id="handle_label">
<property name="visible">True</property>
<property name="label">Handle</property>
<property name="ellipsize">end</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="date_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="opacity">0.5</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Yesterday</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkImage" id="pin_indicator">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="icon_name">view-pin-symbolic</property>
<property name="icon_size">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
<property name="width">3</property>
</packing>
</child>
<child>
<object class="GtkRevealer" id="revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="reveal_child">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="TootleWidgetsRichLabel" id="revealer_content">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.</property>
<property name="wrap">True</property>
<property name="wrap_mode">word-char</property>
<property name="width_chars">15</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="TootleWidgetsAttachmentBox" id="attachments">
<property name="visible">True</property>
<property name="margin_top">8</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
<property name="width">3</property>
</packing>
</child>
<child>
<object class="TootleWidgetsAvatar" id="avatar">
<property name="width_request">48</property>
<property name="height_request">48</property>
<property name="valign">start</property>
<property name="visible">true</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="height">4</property>
</packing>
</child>
<child>
<object class="TootleWidgetsRichLabel" id="header_label">
<property name="visible">True</property>
<property name="ellipsize">end</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
<property name="width">3</property>
</packing>
</child>
<child>
<object class="TootleWidgetsRichLabel" id="content">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.</property>
<property name="wrap">True</property>
<property name="wrap_mode">word-char</property>
<property name="width_chars">15</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
<property name="width">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</template>
</interface>

4
install.sh Executable file
View File

@ -0,0 +1,4 @@
meson build --prefix=/usr
cd build
sudo ninja install
com.github.bleakgrey.tootle

View File

@ -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')

View File

@ -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

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 worlds 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 "<a href=\"%s\"><b>%s</b></a> mentioned you"
#: src/Services/Network.vala:86
msgid "Network Error"
msgstr ""
#: src/API/NotificationType.vala:52
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> boosted your toot"
#: src/API/Visibility.vala:36
msgid "Unlisted"
msgstr ""
#: src/API/NotificationType.vala:54
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> 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 "<a href=\"%s\"><b>%s</b></a> now follows you"
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> mentioned you</span>"
msgstr ""
#: src/API/NotificationType.vala:58
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> wants to follow you"
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> boosted your status</"
"span>"
msgstr ""
#: src/API/NotificationType.vala:60
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> posted a toot"
msgid "<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> boosted</span>"
msgstr ""
#: src/API/Status.vala:174
msgid "Boosted!"
#: src/API/NotificationType.vala:62
#, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> favorited your status</"
"span>"
msgstr ""
#: src/API/Status.vala:176
msgid "Removed boost"
#: src/API/NotificationType.vala:64
#, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> now follows you</span>"
msgstr ""
#: src/API/Status.vala:189
msgid "Favorited!"
#: src/API/NotificationType.vala:66
#, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> wants to follow you</"
"span>"
msgstr ""
#: src/API/Status.vala:191
msgid "Removed from favorites"
#: src/API/NotificationType.vala:68
#, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> posted a status</span>"
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 "<b>New Account</b>"
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 "<a href=\"%s\"><b>%s</b></a> 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 ""

File diff suppressed because it is too large Load Diff

View File

@ -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 worlds 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 "<a href=\"%s\"><b>%s</b></a> mentioned you"
msgstr "<a href=\"%s\"><b>%s</b></a> vous a mentionné"
#: src/Services/Network.vala:86
msgid "Network Error"
msgstr "Erreur réseau"
#: src/API/NotificationType.vala:52
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> boosted your toot"
msgstr "<a href=\"%s\"><b>%s</b></a> a partagé votre pouet"
#: src/API/Visibility.vala:36
msgid "Unlisted"
msgstr ""
#: src/API/NotificationType.vala:54
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> favorited your toot"
msgstr "<a href=\"%s\"><b>%s</b></a> a ajouté votre pouet à ses favoris"
#: src/API/NotificationType.vala:56
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> now follows you"
msgstr "<a href=\"%s\"><b>%s</b></a> vous suit désormais"
#: src/API/NotificationType.vala:58
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> wants to follow you"
msgstr "<a href=\"%s\"><b>%s</b></a> demande à vous suivre"
#: src/API/NotificationType.vala:60
#, fuzzy, c-format
msgid "<a href=\"%s\"><b>%s</b></a> posted a toot"
msgstr "<a href=\"%s\"><b>%s</b></a> 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 ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> mentioned you</span>"
msgstr "<a href=\"%s\"><b>%s</b></a> 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 ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> boosted your status</"
"span>"
msgstr "<a href=\"%s\"><b>%s</b></a> 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 "<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> boosted</span>"
msgstr "<a href=\"%s\"><b>%s</b></a> a partagé"
#: src/Widgets/AccountsButton.vala:83
msgid "Watchlist"
msgstr "Liste de veille"
#: src/API/NotificationType.vala:62
#, fuzzy, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> favorited your status</"
"span>"
msgstr "<a href=\"%s\"><b>%s</b></a> 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 ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> now follows you</span>"
msgstr "<a href=\"%s\"><b>%s</b></a> vous suit désormais"
#: src/Widgets/AccountsButton.vala:142
msgid "<b>New Account</b>"
msgstr "<b>Nouveau Compte</b>"
#: src/API/NotificationType.vala:66
#, fuzzy, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> wants to follow you</"
"span>"
msgstr "<a href=\"%s\"><b>%s</b></a> 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 ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> posted a status</span>"
msgstr "<a href=\"%s\"><b>%s</b></a> 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 "<a href=\"%s\"><b>%s</b></a> boosted"
msgstr "<a href=\"%s\"><b>%s</b></a> 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 "<b>New Account</b>"
#~ msgstr "<b>Nouveau Compte</b>"
#~ 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"

889
po/pl.po

File diff suppressed because it is too large Load Diff

3
po/regenerate-po-files.sh Executable file
View File

@ -0,0 +1,3 @@
cd .
cd build
ninja com.github.bleakgrey.tootle-update-po

734
po/ru.po
View File

@ -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 worlds 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 "<a href=\"%s\"><b>%s</b></a> mentioned you"
msgstr "<a href=\"%s\"><b>%s</b></a> упомянул вас"
#: src/API/NotificationType.vala:52
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> boosted your toot"
msgstr "<a href=\"%s\"><b>%s</b></a> продвинул ваш статус"
#: src/API/NotificationType.vala:54
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> favorited your toot"
msgstr "<a href=\"%s\"><b>%s</b></a> понравился ваш статус"
#: src/API/NotificationType.vala:56
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> now follows you"
msgstr "<a href=\"%s\"><b>%s</b></a> подписался на вас"
#: src/API/NotificationType.vala:58
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> wants to follow you"
msgstr "<a href=\"%s\"><b>%s</b></a> хочет на вас подписаться"
#: src/API/NotificationType.vala:60
#, c-format
msgid "<a href=\"%s\"><b>%s</b></a> posted a toot"
msgstr "<a href=\"%s\"><b>%s</b></a> опубликовал статус"
#: 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 "<b>New Account</b>"
msgstr "<b>Новый аккаунт</b>"
#: 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 "<a href=\"%s\"><b>%s</b></a> boosted"
msgstr "<a href=\"%s\"><b>%s</b></a> продвинул"
#: 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 ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> mentioned you</span>"
msgstr "<a href=\"%s\"><b>%s</b></a> упомянул вас"
#: src/API/NotificationType.vala:58
#, fuzzy, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> boosted your status</"
"span>"
msgstr "<a href=\"%s\"><b>%s</b></a> продвинул ваш статус"
#: src/API/NotificationType.vala:60
#, fuzzy, c-format
msgid "<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> boosted</span>"
msgstr "<a href=\"%s\"><b>%s</b></a> продвинул"
#: src/API/NotificationType.vala:62
#, fuzzy, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> favorited your status</"
"span>"
msgstr "<a href=\"%s\"><b>%s</b></a> понравился ваш статус"
#: src/API/NotificationType.vala:64
#, fuzzy, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> now follows you</span>"
msgstr "<a href=\"%s\"><b>%s</b></a> подписался на вас"
#: src/API/NotificationType.vala:66
#, fuzzy, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> wants to follow you</"
"span>"
msgstr "<a href=\"%s\"><b>%s</b></a> хочет на вас подписаться"
#: src/API/NotificationType.vala:68
#, fuzzy, c-format
msgid ""
"<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> posted a status</span>"
msgstr "<a href=\"%s\"><b>%s</b></a> опубликовал статус"
#: 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 "Статусы"

File diff suppressed because it is too large Load Diff

10
src/.editorconfig Normal file
View File

@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
insert_final_newline = false
charset = utf-8
[*.sh]
indent_size = 4
indent_style = tab

View File

@ -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 ();
}
}

View File

@ -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");

View File

@ -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 () {

View File

@ -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;
}
}

View File

@ -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 _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> mentioned you</span>").printf (account.url, account.display_name);
case REBLOG:
return _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> boosted your toot</span>").printf (account.url, account.display_name);
return _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> boosted your status</span>").printf (account.url, account.display_name);
case REBLOG_REMOTE_USER:
return _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> boosted</span>").printf (account.url, account.display_name);
case FAVORITE:
return _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> favorited your toot</span>").printf (account.url, account.display_name);
return _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> favorited your status</span>").printf (account.url, account.display_name);
case FOLLOW:
return _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> now follows you</span>").printf (account.url, account.display_name);
case FOLLOW_REQUEST:
return _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> wants to follow you</span>").printf (account.url, account.display_name);
case WATCHLIST:
return _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> posted a toot</span>").printf (account.url, account.display_name);
return _("<span underline=\"none\"><a href=\"%s\"><b>%s</b></a> posted a status</span>").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 "";
}
}

View File

@ -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")
);
}
}

View File

@ -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<API.Mention>? mentions { get; set; default = null; }
public ArrayList<API.Attachment>? 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<API.Mention> ();
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<API.Attachment> ();
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 ();
}
}

View File

@ -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")
);
}
}

View File

@ -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};
}
}

View File

@ -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<InstanceAccount> accounts);
public GenericArray<InstanceAccount> saved_accounts = new GenericArray<InstanceAccount> ();
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<InstanceAccount> ();
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);
}
}
}

View File

@ -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 = {"<Ctrl>T"};
public string[] ACCEL_TOGGLE_REVEAL = {"<Ctrl>S"};
public string[] ACCEL_BACK = {"<Alt>BackSpace", "<Alt>Left"};
public string[] ACCEL_REFRESH = {"<Ctrl>R", "F5"};
public string[] ACCEL_TIMELINE_0 = {"<Alt>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 () {

9
src/Build.vala Normal file
View File

@ -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";
}

View File

@ -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) {

View File

@ -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 ();
}
}

View File

@ -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 ();
}

View File

@ -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 ("<a href=\"https://joinmastodon.org/\">%s</a>".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 ();
}
}

View File

@ -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);
}
}

View File

@ -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<ModelItem> items = new GenericArray<ModelItem> ();
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<string> array) {
array.@foreach (item => {
model.append (new ModelItem (item));
return true;
});
list.bind_model (model, create_row);
}
public ListStack (ArrayList<string> 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 ();
}
}

View File

@ -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);
}
}

View File

@ -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 ("&nbsp;", " ")
.replace ("'", "&apos;")
.replace ("& ", "&amp;");
}
public static string restore_entities (string str) {
return str
.replace ("&amp;", "&")
.replace ("&lt;", "<")
.replace ("&gt;", ">")
.replace ("&apos;", "'")
.replace ("&quot;", "\"");
}
public static string simplify (string str) {
var divided = str
.replace("<br>", "\n")
.replace("</br>", "")
.replace("<br />", "\n")
.replace("<p>", "")
.replace("</p>", "\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, ";&+");
}
}

View File

@ -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<CachedImage?, Soup.Message> in_progress;
private GLib.HashTable<CachedImage?, Gdk.Pixbuf> pixbufs;
private uint total_size_est;
private uint size_limit;
private string cache_path;
construct {
pixbufs = new GLib.HashTable<CachedImage?, Gdk.Pixbuf> (CachedImage.hash, CachedImage.equal);
in_progress = new GLib.HashTable<CachedImage?, Soup.Message> (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);
}
}

View File

@ -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<API.Notification> cached_notifications {get; set;}
public int64 last_seen_notification { get; set; default = 0; }
public bool has_unread_notifications { get; set; default = false; }
public ArrayList<API.Notification> cached_notifications { get; set; default = new ArrayList<API.Notification> (); }
private Notificator? notificator;
protected string? stream;
public InstanceAccount () {
cached_notifications = new ArrayList<API.Notification> ();
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;
// });
}
}

View File

@ -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);
});
}
}

View File

@ -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 ();
}
}

110
src/Request.vala Normal file
View File

@ -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<string, string>? 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<string, string> ();
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 ();
}
}

141
src/Services/Accounts.vala Normal file
View File

@ -0,0 +1,141 @@
using Gee;
public class Tootle.Accounts : GLib.Object {
private string dir_path;
private string file_path;
public ArrayList<InstanceAccount> saved { get; set; default = new ArrayList<InstanceAccount> (); }
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);
}
}
}

136
src/Services/Cache.vala Normal file
View File

@ -0,0 +1,136 @@
using Gee;
using Gdk;
public class Tootle.Cache : GLib.Object {
protected HashTable<string, Item> items { get; set; }
protected HashTable<string, Soup.Message> items_in_progress { get; set; }
protected uint size {
get {
return items.size ();
}
}
construct {
items = new HashTable<string, Item> (GLib.str_hash, GLib.str_equal);
items_in_progress = new HashTable<string, Soup.Message> (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 ();
}
}

View File

@ -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<InstanceAccount> accounts) {}
}

View File

@ -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;
}
}

88
src/Services/Network.vala Normal file
View File

@ -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 ();
}
}

View File

@ -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);
}
}

177
src/Services/Streams.vala Normal file
View File

@ -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<string, Connection> connections {
get;
set;
default = new HashTable<string, Connection> (GLib.str_hash, GLib.str_equal);
}
protected class Connection : Object {
public ArrayList<IStreamListener> 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<IStreamListener> ();
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;
}
}
}

View File

@ -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");
}
}

651
src/Stacktrace.vala Normal file
View File

@ -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<Frame> _frames = new Gee.ArrayList<Frame>();
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<Frame> 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 = "<unknown> " + 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 <unknown> %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);
}
}

16
src/Utils.vala Normal file
View File

@ -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) ;
}
}
}
}

View File

@ -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;
}
}

89
src/Views/Base.vala Normal file
View File

@ -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 = @"<span size='large'>$status_message</span>";
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";
}
}

View File

@ -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)";
}
}

View File

@ -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 ();
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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);
}
});
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

183
src/Views/NewAccount.vala Normal file
View File

@ -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 ();
}
}

View File

@ -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;
}
}

View File

@ -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 (@"<span size='x-large' weight='bold'>$label</span>");
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 (@"<b>$val</b>"));
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 (@"<b>$val</b>"));
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 (@"<b>$val</b>"));
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 ("<b>%s</b>".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 ("<b>%s</b>\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);

View File

@ -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 (@"<span weight='bold' size='medium'>$name</span>");
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 = "<a href=\"%s/tags/%s\">#%s</a>".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 (@"<a href=\"$(accounts.active.instance)/tags/$encoded\">#$name</a>");
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 ();
}
}

View File

@ -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 ();

View File

@ -1,125 +0,0 @@
using GLib;
using Gee;
public class Tootle.Watchlist : Object {
public ArrayList<string> users = new ArrayList<string> ();
public ArrayList<string> hashtags = new ArrayList<string> ();
public ArrayList<Notificator> notificators = new ArrayList<Notificator> ();
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);
}
}
}

View File

@ -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 = "<a href=\"%s\">@%s</a>".printf (account.url, account.acct);
status.created_at = account.created_at;
//status.url = account.url;
//status.content = "<a href=\"%s\">@%s</a>".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) {

View File

@ -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<InstanceAccount> accounts) {
list.forall (widget => widget.destroy ());
int i = -1;
accounts.foreach (account => {
i++;
var widget = new AccountItemView ();
widget.id = i;
widget.display_name.label = "<b>@"+account.username+"</b>";
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 = _("<b>New Account</b>");
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<InstanceAccount> 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 = "<b>" + _("No active account") + "</b>";
}
else {
avatar.url = account.avatar;
item_accounts.text = @"<b>$(account.display_name)</b>\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;
}
}

View File

@ -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<API.Attachment>? 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;
}
}

Some files were not shown because too many files have changed in this diff Show More