├── .gitmodules ├── .gitignore ├── meson_options.txt ├── ui ├── Media │ ├── MediaDialog.blp │ ├── MediaDisplayItem.blp │ ├── MediaSelector.blp │ └── MediaDisplay.blp ├── Preferences │ ├── PreferencesWindow.blp │ ├── SessionsPage.blp │ ├── AppearancesPage.blp │ └── SessionSettings.blp ├── Authentication │ ├── LoadPage.blp │ ├── FinalPage.blp │ ├── StartPage.blp │ ├── CodePage.blp │ ├── BrowserPage.blp │ ├── ServerPage.blp │ └── AuthView.blp ├── Pages │ ├── ThreadPage.blp │ ├── UserPage.blp │ └── MainPage.blp ├── Content │ ├── UserDataDisplay.blp │ ├── PostMetrics.blp │ ├── UserCard.blp │ ├── PostActions.blp │ ├── PostStatus.blp │ ├── PostContent.blp │ ├── PostItem.blp │ └── UserDisplay.blp ├── Widgets │ ├── WaitingButton.blp │ ├── SessionRow.blp │ ├── UserAvatar.blp │ ├── FilterButton.blp │ ├── UserButton.blp │ ├── SessionSidebar.blp │ └── BadgesBox.blp ├── MainWindow.blp ├── Collections │ ├── CollectionFilter.blp │ └── CollectionView.blp ├── style-dark.css ├── style-hc.css ├── meson.build └── interface.gresource.xml ├── subprojects └── blueprint-compiler.wrap ├── data ├── uk.co.ibboard.Cawbird.desktop ├── icons │ ├── scalable │ │ ├── actions │ │ │ ├── appearance-symbolic.svg │ │ │ ├── person-symbolic.svg │ │ │ ├── people-symbolic.svg │ │ │ ├── animated-symbolic.svg │ │ │ ├── reload-symbolic.svg │ │ │ ├── protected-user-symbolic.svg │ │ │ ├── stopwatch-symbolic.svg │ │ │ ├── reply-symbolic.svg │ │ │ ├── verified-symbolic.svg │ │ │ ├── repost-symbolic.svg │ │ │ ├── platform-mastodon-symbolic.svg │ │ │ └── reposted-symbolic.svg │ │ └── apps │ │ │ └── uk.co.ibboard.Cawbird.svg │ ├── meson.build │ ├── icons.gresource.xml │ └── symbolic │ │ └── apps │ │ ├── uk.co.ibboard.Cawbird-symbolic.svg │ │ └── uk.co.ibboard.Cawbird.Devel-symbolic.svg ├── uk.co.ibboard.Cawbird.gschema.xml ├── uk.co.ibboard.Cawbird.appdata.xml └── meson.build ├── lib ├── Base │ ├── Content │ │ ├── MediaEnums.vala │ │ ├── UserEnums.vala │ │ ├── UserDataField.vala │ │ ├── TextModule.vala │ │ └── PostAuxiliary.vala │ ├── Collections │ │ ├── CollectionPins.vala │ │ ├── CollectionFilters.vala │ │ ├── CollectionCalls.vala │ │ └── CollectionHeaders.vala │ ├── System │ │ └── SessionAuth.vala │ ├── Utils │ │ ├── StateIO.vala │ │ └── TextFormats.vala │ └── Organization │ │ └── Thread.vala ├── Mastodon │ ├── Utils │ │ └── ParseUtils.vala │ ├── Content │ │ ├── UserDataField.vala │ │ └── User.vala │ └── Organization │ │ ├── HomeTimeline.vala │ │ └── Thread.vala └── meson.build ├── README.md ├── tests ├── meson.build └── Parsing │ ├── UserData │ └── Mastodon │ │ ├── BasicUser.json │ │ └── BasicChecks.json │ ├── MediaData │ └── Mastodon │ │ ├── OnePictureChecks.json │ │ ├── TwoPictureChecks.json │ │ ├── ThreePictureChecks.json │ │ └── FourPictureChecks.json │ ├── PostData │ └── Mastodon │ │ ├── BasicChecks.json │ │ ├── BasicPost.json │ │ ├── RepostChecks.json │ │ ├── HashtagsChecks.json │ │ └── HashtagsPost.json │ ├── UserParsing.vala │ ├── MediaChecks.vala │ ├── PostParsing.vala │ └── MediaParsing.vala ├── src ├── config.vapi ├── Widgets │ ├── FilterButton.vala │ ├── BadgesBox.vala │ ├── SessionRow.vala │ ├── WaitingButton.vala │ └── CroppedPicture.vala ├── Authentication │ ├── FinalPage.vala │ ├── StartPage.vala │ └── LoadPage.vala ├── Preferences │ ├── AppearancesPage.vala │ ├── PreferencesWindow.vala │ └── SessionsPage.vala ├── Content │ ├── PostMetrics.vala │ └── UserDataDisplay.vala ├── Collections │ └── CollectionFilter.vala ├── Pages │ ├── UserPage.vala │ ├── ThreadPage.vala │ └── MainPage.vala └── meson.build ├── .github └── workflows │ └── flatpak.yml └── meson.build /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "translations"] 2 | path = subprojects/po 3 | url = https://github.com/CodedOre/NewCaw-po.git 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Do not track our copy of the blueprint compiler 2 | /subprojects/blueprint-compiler 3 | 4 | # Files specific to Gnome Builder 5 | .buildconfig 6 | .dev/ -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option( 2 | 'backends', 3 | type: 'combo', 4 | choices: [ 'Mastodon', 'Full' ], 5 | value: 'Full', 6 | description: 'The backends that are included in the build.' 7 | ) -------------------------------------------------------------------------------- /ui/Media/MediaDialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template MediaDialog : Adw.Window { 5 | modal: true; 6 | 7 | Adw.ToastOverlay toasts { 8 | .MediaDisplay media_display { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /subprojects/blueprint-compiler.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | directory = blueprint-compiler 3 | url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git 4 | revision = main 5 | depth = 1 6 | 7 | [provide] 8 | program_names = blueprint-compiler -------------------------------------------------------------------------------- /ui/Preferences/PreferencesWindow.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template PreferencesWindow : Adw.PreferencesWindow { 5 | can-navigate-back: true; 6 | 7 | .PreferencesAppearancesPage {} 8 | .PreferencesSessionsPage {} 9 | } -------------------------------------------------------------------------------- /ui/Media/MediaDisplayItem.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | template MediaDisplayItem : Gtk.Widget { 4 | hexpand: true; 5 | vexpand: true; 6 | layout-manager: Gtk.BinLayout {}; 7 | 8 | Gtk.Picture content {} 9 | } 10 | -------------------------------------------------------------------------------- /ui/Authentication/LoadPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template AuthenticationLoadPage : Gtk.Widget { 5 | hexpand: true; 6 | layout-manager: Gtk.BinLayout {}; 7 | 8 | Adw.StatusPage page_content { 9 | icon-name: "user-info-symbolic"; 10 | title: _("Finishing Authentication..."); 11 | 12 | Gtk.Spinner load_indicator {} 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/Pages/ThreadPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template ThreadPage : Gtk.Widget { 5 | layout-manager: Gtk.BoxLayout { 6 | orientation: vertical; 7 | }; 8 | 9 | Adw.HeaderBar page_header { 10 | [start] 11 | Gtk.Button { 12 | icon-name: "go-previous-symbolic"; 13 | action-name: "main.move-back"; 14 | } 15 | 16 | [title] 17 | Adw.WindowTitle page_title {} 18 | } 19 | 20 | .CollectionView collection_view {} 21 | } -------------------------------------------------------------------------------- /ui/Content/UserDataDisplay.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template UserDataDisplay : Gtk.Widget { 5 | layout_manager: Gtk.BoxLayout { 6 | spacing: 6; 7 | }; 8 | 9 | Gtk.Image verified_icon { 10 | icon-name: "verified-symbolic"; 11 | } 12 | 13 | Gtk.Label name_label { 14 | styles [ 15 | "heading" 16 | ] 17 | } 18 | 19 | Gtk.Label content_label { 20 | use-markup: true; 21 | ellipsize: end; 22 | 23 | activate-link => on_link_clicked (); 24 | } 25 | } -------------------------------------------------------------------------------- /ui/Pages/UserPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template UserPage : Gtk.Widget { 5 | layout-manager: Gtk.BoxLayout { 6 | orientation: vertical; 7 | }; 8 | 9 | Adw.HeaderBar page_header { 10 | [start] 11 | Gtk.Button { 12 | icon-name: "go-previous-symbolic"; 13 | action-name: "main.move-back"; 14 | } 15 | 16 | [title] 17 | Adw.WindowTitle page_title {} 18 | } 19 | 20 | .CollectionView collection_view { 21 | header: .UserDisplay { 22 | user: bind UserPage.user; 23 | }; 24 | } 25 | } -------------------------------------------------------------------------------- /ui/Widgets/WaitingButton.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template WaitingButton : Gtk.Widget { 5 | layout-manager: Gtk.BinLayout {}; 6 | 7 | Gtk.Stack waiting_stack { 8 | Gtk.Box button_content { 9 | halign: center; 10 | spacing: 8; 11 | 12 | Gtk.Image button_icon { 13 | visible: false; 14 | icon-name: bind WaitingButton.icon-name; 15 | } 16 | 17 | Gtk.Label button_label { 18 | visible: false; 19 | label: bind WaitingButton.label; 20 | } 21 | } 22 | 23 | Gtk.Spinner waiting_spinner {} 24 | } 25 | } -------------------------------------------------------------------------------- /data/uk.co.ibboard.Cawbird.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | # TRANSLATORS: Set by meson, do not translate 3 | Name=@app_name@ 4 | GenericName=Mastodon Client 5 | Comment=Your window to the social world 6 | # TRANSLATORS: Search terms to find this application, separated by semicolons. The list must end with a semicolon! 7 | Keywords=mastodon; 8 | Exec=cawbird %U 9 | Type=Application 10 | Icon=@app_id@ 11 | Categories=Network;GTK; 12 | # DBusActivatable=true 13 | StartupNotify=true 14 | X-Purism-FormFactor=Workstation;Mobile; 15 | # Custom uri scheme to receive authentication callbacks 16 | MimeType=x-scheme-handler/cawbird -------------------------------------------------------------------------------- /ui/Authentication/FinalPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template AuthenticationFinalPage : Gtk.Widget { 5 | hexpand: true; 6 | layout-manager: Gtk.BinLayout {}; 7 | 8 | Adw.StatusPage page_content { 9 | icon-name: "emblem-ok-symbolic"; 10 | title: _("Authentication Complete!"); 11 | 12 | Gtk.Button { 13 | label: _("Continue"); 14 | halign: center; 15 | width-request: 275; 16 | 17 | clicked => on_continue (); 18 | 19 | styles [ 20 | "pill", 21 | "suggested-action" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /data/icons/scalable/actions/appearance-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/scalable/actions/person-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /ui/Preferences/SessionsPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template PreferencesSessionsPage : Adw.PreferencesPage { 5 | title: _("Accounts"); 6 | icon-name: "people-symbolic"; 7 | 8 | Adw.PreferencesGroup { 9 | Gtk.ListBox session_list { 10 | selection-mode: none; 11 | 12 | row-activated => display_session_settings (); 13 | 14 | styles [ 15 | "boxed-list" 16 | ] 17 | } 18 | } 19 | 20 | Adw.PreferencesGroup { 21 | Adw.ActionRow { 22 | activatable: true; 23 | title: _("Add Account"); 24 | icon-name: "list-add-symbolic"; 25 | action-name: "preferences.add-session"; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /ui/Widgets/SessionRow.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template SessionRow : Adw.ActionRow { 5 | activatable: true; 6 | 7 | [prefix] 8 | .UserAvatar account_avatar { 9 | indicate-platform: true; 10 | size: 42; 11 | } 12 | 13 | Gtk.Button { 14 | halign: center; 15 | valign: center; 16 | icon-name: "window-new-symbolic"; 17 | tooltip-text: _("Open in new Window"); 18 | visible: bind SessionRow.show-actions; 19 | 20 | clicked => open_in_window (); 21 | 22 | styles [ 23 | "flat" 24 | ] 25 | } 26 | 27 | Gtk.Image { 28 | icon-name: "go-next-symbolic"; 29 | visible: bind SessionRow.show-next; 30 | } 31 | } -------------------------------------------------------------------------------- /ui/Widgets/UserAvatar.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template UserAvatar : Gtk.Widget { 5 | layout-manager: Gtk.BinLayout {}; 6 | 7 | Adw.Avatar avatar_holder { 8 | text: "default_avatar"; 9 | size: bind UserAvatar.size; 10 | } 11 | 12 | Gtk.Image platform_indicator { 13 | visible: bind UserAvatar.indicate-platform; 14 | halign: end; 15 | valign: end; 16 | 17 | styles [ 18 | "platform-indicator" 19 | ] 20 | } 21 | 22 | Gtk.Button avatar_selector { 23 | visible: bind UserAvatar.main-mode; 24 | action-name: "avatar.display_media"; 25 | 26 | styles [ 27 | "avatar-button", 28 | "flat" 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /ui/Widgets/FilterButton.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template FilterButton : Gtk.Widget { 5 | layout-manager: Gtk.BinLayout {}; 6 | 7 | Gtk.ToggleButton button { 8 | active: bind FilterButton.active; 9 | 10 | toggled => on_toggled (); 11 | 12 | styles [ 13 | "filter-pill" 14 | ] 15 | 16 | Gtk.Box { 17 | Gtk.Revealer { 18 | reveal-child: bind FilterButton.active; 19 | transition-type: slide_right; 20 | Gtk.Image { 21 | margin-end: 4; 22 | icon-name: "object-select-symbolic"; 23 | } 24 | } 25 | 26 | Gtk.Label { 27 | label: bind FilterButton.label; 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ui/Authentication/StartPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template AuthenticationStartPage : Gtk.Widget { 5 | hexpand: true; 6 | layout-manager: Gtk.BinLayout {}; 7 | 8 | Adw.StatusPage page_content { 9 | icon-name: "system-users-symbolic"; 10 | title: _("Add an Account"); 11 | 12 | Adw.Clamp { 13 | maximum-size: 380; 14 | 15 | Gtk.Box { 16 | orientation: vertical; 17 | 18 | Gtk.Button mastodon_button { 19 | visible: false; 20 | label: _("Add Mastodon Account"); 21 | 22 | styles [ 23 | "mastodon-background", 24 | "pill", 25 | "opaque", 26 | ] 27 | } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ui/MainWindow.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template MainWindow : Adw.ApplicationWindow { 5 | width-request: 360; 6 | height-request: 250; 7 | default-width: 650; 8 | default-height: 450; 9 | 10 | Gtk.Stack window_stack { 11 | 12 | Gtk.StackPage { 13 | name: "auth"; 14 | title: "AuthView"; 15 | child: .AuthView auth_view {}; 16 | } 17 | 18 | Gtk.StackPage { 19 | name: "main"; 20 | title: "MainView"; 21 | child: Adw.Leaflet main_view { 22 | can-navigate-back: true; 23 | can-unfold: false; 24 | 25 | notify::child-transition-running => on_transition (); 26 | 27 | .MainPage main_page {} 28 | }; 29 | } 30 | 31 | } 32 | } -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | # Cawbird Icons build file 2 | 3 | # Install application icon 4 | scalable_dir = join_paths('scalable', 'apps') 5 | install_data( 6 | join_paths(scalable_dir, ('@0@.svg').format(application_id)), 7 | install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', scalable_dir) 8 | ) 9 | 10 | # Install symbolic app icon 11 | symbolic_dir = join_paths('symbolic', 'apps') 12 | install_data( 13 | join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)), 14 | install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', symbolic_dir) 15 | ) 16 | 17 | # Install UI icons to resources 18 | cawresources += gnome.compile_resources( 19 | 'cawbird_icon_resources', 20 | 'icons.gresource.xml', 21 | c_name: 'cawbird_icons' 22 | ) 23 | -------------------------------------------------------------------------------- /ui/Widgets/UserButton.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template UserButton : Gtk.Widget { 5 | layout-manager: Gtk.BinLayout {}; 6 | 7 | Gtk.Button selector { 8 | clicked => on_selected (); 9 | 10 | styles [ 11 | "flat" 12 | ] 13 | 14 | Gtk.Box button_content { 15 | spacing: 4; 16 | 17 | Gtk.Image { 18 | visible: bind UserButton.is-repost; 19 | icon-name: "repost-symbolic"; 20 | } 21 | 22 | Gtk.Label display_label { 23 | ellipsize: end; 24 | } 25 | 26 | .BadgesBox user_badges {} 27 | 28 | Gtk.Label username_label { 29 | ellipsize: end; 30 | 31 | styles [ 32 | "body", 33 | "dim-label" 34 | ] 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /data/icons/scalable/actions/people-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/scalable/actions/animated-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /data/icons/scalable/actions/reload-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /ui/Content/PostMetrics.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template PostMetrics : Gtk.Widget { 5 | layout-manager: Gtk.BoxLayout { 6 | spacing: 16; 7 | }; 8 | 9 | Gtk.Box likes_box { 10 | spacing: 6; 11 | 12 | Gtk.Image { 13 | icon-name: "not-liked-symbolic"; 14 | } 15 | 16 | Gtk.Label likes_counter { 17 | styles [ 18 | "heading" 19 | ] 20 | } 21 | } 22 | 23 | Gtk.Box reposts_box { 24 | spacing: 6; 25 | 26 | Gtk.Image { 27 | icon-name: "repost-symbolic"; 28 | } 29 | 30 | Gtk.Label reposts_counter { 31 | styles [ 32 | "heading" 33 | ] 34 | } 35 | } 36 | 37 | Gtk.Box replies_box { 38 | spacing: 6; 39 | 40 | Gtk.Image { 41 | icon-name: "reply-symbolic"; 42 | } 43 | 44 | Gtk.Label replies_counter { 45 | styles [ 46 | "heading" 47 | ] 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /ui/Collections/CollectionFilter.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template CollectionFilter : Gtk.Widget { 5 | layout-manager: Gtk.BinLayout {}; 6 | margin-top: 24; 7 | margin-bottom: 16; 8 | 9 | Gtk.FlowBox filter_box { 10 | column-spacing: 4; 11 | max-children-per-line: 42; 12 | selection-mode: none; 13 | 14 | .FilterButton generic_filter { 15 | active: bind CollectionFilter.display_generic bidirectional; 16 | label: _("Posts"); 17 | } 18 | 19 | .FilterButton replies_filter { 20 | active: bind CollectionFilter.display_replies bidirectional; 21 | label: _("Replies"); 22 | } 23 | 24 | .FilterButton reposts_filter { 25 | active: bind CollectionFilter.display_reposts bidirectional; 26 | label: _("Reposts"); 27 | } 28 | 29 | .FilterButton media_filter { 30 | active: bind CollectionFilter.display_media bidirectional; 31 | label: _("Media"); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /ui/Collections/CollectionView.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template CollectionView : Gtk.Widget { 5 | layout-manager: Gtk.BinLayout {}; 6 | vexpand: true; 7 | 8 | styles [ 9 | "view" 10 | ] 11 | 12 | Gtk.ScrolledWindow scroll_window { 13 | hscrollbar-policy: never; 14 | 15 | Adw.ClampScrollable { 16 | maximum-size: 1024; 17 | tightening-threshold: 512; 18 | 19 | Gtk.ListView listview { 20 | activate => on_activation (); 21 | 22 | styles [ 23 | "post-list" 24 | ] 25 | } 26 | } 27 | } 28 | } 29 | 30 | Gtk.Separator list_separator { 31 | orientation: vertical; 32 | } 33 | 34 | .CollectionFilter filter_options {} 35 | 36 | Adw.ActionRow timeout_indicator { 37 | icon-name: "stopwatch-symbolic"; 38 | title: _("Replies can't be pulled."); 39 | subtitle: _("The API can't pull replies for posts older than 7 days."); 40 | 41 | styles [ 42 | "warning" 43 | ] 44 | } -------------------------------------------------------------------------------- /data/icons/scalable/actions/protected-user-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/Content/UserCard.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template UserCard : Gtk.Widget { 5 | height-request: 280; 6 | layout-manager: Gtk.BinLayout {}; 7 | 8 | .MediaSelector user_banner { 9 | height-request: 220; 10 | valign: start; 11 | only-preview: false; 12 | action-name: "UserCard.display_header"; 13 | 14 | styles [ 15 | "roundable" 16 | ] 17 | } 18 | 19 | Gtk.Box infobox { 20 | margin-start: 20; 21 | margin-end: 20; 22 | spacing: 8; 23 | valign: end; 24 | 25 | .UserAvatar user_avatar { 26 | size: 96; 27 | main-mode: true; 28 | hexpand: true; 29 | halign: start; 30 | } 31 | 32 | Gtk.Button follow_button { 33 | width-request: 120; 34 | margin-bottom: 12; 35 | valign: end; 36 | } 37 | 38 | Gtk.MenuButton options_button { 39 | icon-name: "view-more-symbolic"; 40 | margin-bottom: 12; 41 | valign: end; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /data/icons/scalable/actions/stopwatch-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /ui/style-dark.css: -------------------------------------------------------------------------------- 1 | /* style-dark.css 2 | * 3 | * Copyright 2021 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | /** 22 | * Custom styles used by Cawbird when using the dark style. 23 | */ 24 | 25 | /** 26 | * Color override for bot badge. 27 | */ 28 | badge.bot { 29 | color: @dark_4; 30 | background: @light_4; 31 | } 32 | -------------------------------------------------------------------------------- /ui/Widgets/SessionSidebar.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template SessionSidebar : Gtk.Widget { 5 | margin-top: 8; 6 | margin-bottom: 8; 7 | margin-start: 8; 8 | margin-end: 8; 9 | layout-manager: Gtk.BoxLayout { 10 | orientation: vertical; 11 | spacing: 12; 12 | }; 13 | 14 | Gtk.ListBox active_list { 15 | selection-mode: none; 16 | 17 | styles [ 18 | "session-list" 19 | ] 20 | 21 | .SessionRow { 22 | session: bind SessionSidebar.active-session; 23 | show-actions: false; 24 | } 25 | 26 | Adw.ActionRow { 27 | activatable: true; 28 | icon-name: "person-symbolic"; 29 | title: _("Display Account"); 30 | 31 | activated => display_session_account (); 32 | 33 | styles [ 34 | "option-row" 35 | ] 36 | } 37 | } 38 | 39 | Gtk.Separator sidebar_separator {} 40 | 41 | Gtk.ListBox session_list { 42 | selection-mode: none; 43 | 44 | row-activated => change_active_session (); 45 | 46 | styles [ 47 | "session-list" 48 | ] 49 | } 50 | } -------------------------------------------------------------------------------- /data/icons/icons.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scalable/actions/animated-symbolic.svg 5 | scalable/actions/appearance-symbolic.svg 6 | scalable/actions/bot-user-symbolic.svg 7 | scalable/actions/liked-symbolic.svg 8 | scalable/actions/not-liked-symbolic.svg 9 | scalable/actions/people-symbolic.svg 10 | scalable/actions/person-symbolic.svg 11 | scalable/actions/platform-mastodon-symbolic.svg 12 | scalable/actions/protected-user-symbolic.svg 13 | scalable/actions/reload-symbolic.svg 14 | scalable/actions/reply-symbolic.svg 15 | scalable/actions/repost-symbolic.svg 16 | scalable/actions/reposted-symbolic.svg 17 | scalable/actions/stopwatch-symbolic.svg 18 | scalable/actions/verified-symbolic.svg 19 | 20 | 21 | -------------------------------------------------------------------------------- /data/icons/scalable/actions/reply-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/Content/PostActions.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template PostActions : Gtk.Widget { 5 | layout-manager: Gtk.BoxLayout { 6 | spacing: 8; 7 | }; 8 | 9 | Gtk.Button likes_button { 10 | clicked => like_post (); 11 | 12 | styles [ 13 | "condense", 14 | "flat" 15 | ] 16 | 17 | Adw.ButtonContent likes_counter { 18 | icon-name: "not-liked-symbolic"; 19 | } 20 | } 21 | 22 | Gtk.Button reposts_button { 23 | clicked => repost_post (); 24 | 25 | styles [ 26 | "condense", 27 | "flat" 28 | ] 29 | 30 | Adw.ButtonContent reposts_counter { 31 | icon-name: "repost-symbolic"; 32 | } 33 | } 34 | 35 | Gtk.Button replies_button { 36 | sensitive: false; 37 | 38 | styles [ 39 | "condense", 40 | "flat" 41 | ] 42 | 43 | Adw.ButtonContent replies_counter { 44 | icon-name: "reply-symbolic"; 45 | } 46 | } 47 | 48 | Gtk.MenuButton options_button { 49 | direction: up; 50 | icon-name: "view-more-symbolic"; 51 | 52 | styles [ 53 | "flat" 54 | ] 55 | } 56 | } -------------------------------------------------------------------------------- /ui/Widgets/BadgesBox.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template BadgesBox : Gtk.Box { 5 | visible: false; 6 | halign: center; 7 | valign: center; 8 | spacing: 2; 9 | 10 | Gtk.Image author_verified_badge { 11 | visible: bind BadgesBox.display-verified; 12 | pixel-size: bind BadgesBox.icon-size; 13 | css-name: "badge"; 14 | icon-name: "emblem-ok-symbolic"; 15 | tooltip-text: _("Verified User"); 16 | styles [ 17 | "verified" 18 | ] 19 | } 20 | 21 | Gtk.Image author_bot_badge { 22 | visible: bind BadgesBox.display-bot; 23 | pixel-size: bind BadgesBox.icon-size; 24 | css-name: "badge"; 25 | icon-name: "bot-user-symbolic"; 26 | tooltip-text: _("Bot Account"); 27 | styles [ 28 | "bot" 29 | ] 30 | } 31 | 32 | Gtk.Image author_protected_badge { 33 | visible: bind BadgesBox.display-protected; 34 | pixel-size: bind BadgesBox.icon-size; 35 | css-name: "badge"; 36 | icon-name: "protected-user-symbolic"; 37 | tooltip-text: _("Protected User"); 38 | styles [ 39 | "protected" 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /data/uk.co.ibboard.Cawbird.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Display user avatars round. 8 | 9 | true 10 | 11 | 12 | 13 | If to show tags after the text. 14 | 15 | false 16 | 17 | 18 | 19 | If a double click is required to activate posts. 20 | 21 | false 22 | 23 | 24 | 25 | 26 | 27 | 28 | The Size for the MediaDialog. 29 | 30 | (650,450) 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/Base/Content/MediaEnums.vala: -------------------------------------------------------------------------------- 1 | /* MediaEnums.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Stores the type for this media. 25 | */ 26 | public enum Backend.MediaType { 27 | 28 | /** 29 | * A static image. 30 | */ 31 | PICTURE, 32 | 33 | /** 34 | * An image with looped animation. 35 | */ 36 | ANIMATED, 37 | 38 | /** 39 | * A video with sound. 40 | */ 41 | VIDEO 42 | } 43 | -------------------------------------------------------------------------------- /data/icons/scalable/actions/verified-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Warning** 2 | > 3 | > This project is no longer being worked on, see [this post](https://mastodon.online/@CodedOre/110395875219210402) for details. 4 | 5 | # NewCaw 6 | This may be the future Cawbird! 7 | 8 | ## Introduction 9 | 10 | This is the repository for the ongoing work on NewCaw, a rewrite of [Cawbird](https://github.com/ibboard/cawbird). 11 | 12 | It covers the following goals: 13 | - Rewrite the UI in GTK4 and libadwaita, following the current Gnome HIG 14 | - Rewrite the backend in a better extendable structure in Vala 15 | - ~~Provide support for the upcoming API v2.0 for Twitter~~ (dropped after Twitter banned third-party clients) 16 | - Provide support for the Mastodon API 17 | 18 | When the work will be completed, the code in this repo will become Cawbird 2.0. 19 | 20 | ## Building 21 | 22 | The recommended way to build this project is with Gnome Builder, which will use flatpak-builder to get the dependencies and build it. 23 | 24 | ## Contributing 25 | 26 | This is a large project, so every help is appreciated! You can always review the code or take a look at the posted issues. There are also features that are yet to be implemented where you can work on, so get in touch if you want to help with that! 27 | -------------------------------------------------------------------------------- /ui/Preferences/AppearancesPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template PreferencesAppearancesPage : Adw.PreferencesPage { 5 | title: _("Appearance"); 6 | icon-name: "appearance-symbolic"; 7 | 8 | Adw.PreferencesGroup { 9 | Adw.PreferencesRow { 10 | can-focus: false; 11 | can-target: false; 12 | 13 | .PostItem example_post_item {} 14 | } 15 | } 16 | 17 | Adw.PreferencesGroup { 18 | title: _("Posts"); 19 | 20 | Adw.ActionRow { 21 | title: _("Round Avatars"); 22 | activatable-widget: round_avatar_switch; 23 | 24 | Gtk.Switch round_avatar_switch { 25 | valign: center; 26 | halign: center; 27 | } 28 | } 29 | 30 | Adw.ActionRow { 31 | title: _("Display Trailing Tags"); 32 | activatable-widget: trailing_tags_switch; 33 | 34 | Gtk.Switch trailing_tags_switch { 35 | valign: center; 36 | halign: center; 37 | } 38 | } 39 | 40 | Adw.ActionRow { 41 | title: _("Activate on Double-Click"); 42 | activatable-widget: double_click_activation_switch; 43 | 44 | Gtk.Switch double_click_activation_switch { 45 | valign: center; 46 | halign: center; 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /ui/Authentication/CodePage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template AuthenticationCodePage : Gtk.Widget { 5 | hexpand: true; 6 | layout-manager: Gtk.BinLayout {}; 7 | 8 | Adw.ToastOverlay page_content { 9 | 10 | Adw.StatusPage { 11 | icon-name: "dialog-password-symbolic"; 12 | title: _("Enter the Passcode"); 13 | 14 | Adw.Clamp { 15 | maximum-size: 450; 16 | 17 | Gtk.ListBox { 18 | selection-mode: none; 19 | 20 | styles [ 21 | "boxed-list", 22 | ] 23 | 24 | Adw.EntryRow code_entry { 25 | title: _("Authentication Code"); 26 | 27 | changed => on_input (); 28 | entry-activated => on_confirm (); 29 | 30 | Gtk.Button confirm_button { 31 | halign: center; 32 | valign: center; 33 | sensitive: false; 34 | 35 | clicked => on_confirm (); 36 | 37 | styles [ 38 | "circular", 39 | ] 40 | 41 | .WaitingButton button_waiting { 42 | icon-name: "go-next-symbolic"; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /ui/Content/PostStatus.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template PostStatus : Gtk.Widget { 5 | height-request: 36; 6 | layout-manager: Gtk.BoxLayout { 7 | orientation: vertical; 8 | }; 9 | 10 | Adw.Bin previous_line_bin { 11 | width-request: 48; 12 | height-request: 12; 13 | halign: start; 14 | 15 | Gtk.Separator previous_line { 16 | visible: bind PostStatus.show_previous; 17 | halign: center; 18 | orientation: vertical; 19 | } 20 | } 21 | 22 | Gtk.Box information_box { 23 | spacing: 8; 24 | 25 | .UserAvatar user_avatar { 26 | width-request: 48; 27 | halign: center; 28 | size: 48; 29 | } 30 | 31 | Gtk.Box information_text_box { 32 | spacing: 8; 33 | valign: start; 34 | 35 | .UserButton user_button { 36 | valign: center; 37 | is-repost: bind PostStatus.is-repost; 38 | } 39 | 40 | Gtk.Label time_spacer { 41 | visible: bind PostStatus.show-time; 42 | label: "·"; 43 | 44 | styles [ 45 | "dim-label" 46 | ] 47 | } 48 | 49 | Gtk.Label time_label { 50 | visible: bind PostStatus.show-time; 51 | 52 | styles [ 53 | "dim-label" 54 | ] 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /tests/meson.build: -------------------------------------------------------------------------------- 1 | # Tests for the backend library 2 | 3 | # Define all tests with category and files 4 | libtests = { 5 | 'Parsing': { 6 | 'MediaParsing': [ 7 | 'TestUtils.vala', 8 | 'Parsing/MediaChecks.vala', 9 | 'Parsing/MediaParsing.vala', 10 | 'Parsing/PostChecks.vala', 11 | 'Parsing/UserChecks.vala' 12 | ], 13 | 'PostParsing': [ 14 | 'TestUtils.vala', 15 | 'Parsing/PostChecks.vala', 16 | 'Parsing/PostParsing.vala', 17 | 'Parsing/UserChecks.vala' 18 | ], 19 | 'UserParsing': [ 20 | 'TestUtils.vala', 21 | 'Parsing/UserChecks.vala', 22 | 'Parsing/UserParsing.vala' 23 | ] 24 | } 25 | } 26 | 27 | # Set the test dependencies 28 | testdepends = declare_dependency( 29 | link_with: backendlib, 30 | include_directories: backendsrc, 31 | dependencies: libdepends 32 | ) 33 | 34 | # Build and run each test 35 | foreach category, tests : libtests 36 | foreach name, sources : tests 37 | testcase = executable( 38 | name, 39 | sources, 40 | dependencies: testdepends 41 | ) 42 | test( 43 | name, 44 | testcase, 45 | suite: category, 46 | workdir: meson.project_source_root() + '/tests/' + category 47 | ) 48 | endforeach 49 | endforeach 50 | -------------------------------------------------------------------------------- /ui/Authentication/BrowserPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template AuthenticationBrowserPage : Gtk.Widget { 5 | hexpand: true; 6 | layout-manager: Gtk.BinLayout {}; 7 | 8 | Adw.StatusPage page_content { 9 | icon-name: "web-browser-symbolic"; 10 | title: _("Authenticate Cawbird"); 11 | description: _("Continue the authentication in your browser."); 12 | 13 | Gtk.Box { 14 | orientation: vertical; 15 | spacing: 12; 16 | 17 | Gtk.Button continue_button { 18 | visible: false; 19 | label: _("Continue"); 20 | halign: center; 21 | width-request: 275; 22 | sensitive: bind retry_waiting.waiting inverted; 23 | 24 | clicked => on_continue (); 25 | 26 | styles [ 27 | "suggested-action", 28 | "pill" 29 | ] 30 | } 31 | 32 | Gtk.Button retry_button { 33 | halign: center; 34 | width-request: 275; 35 | 36 | clicked => on_retry (); 37 | 38 | styles [ 39 | "flat", 40 | "warning" 41 | ] 42 | 43 | .WaitingButton retry_waiting { 44 | icon-name: "reload-symbolic"; 45 | label: _("Retry Authentication"); 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ui/Authentication/ServerPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template AuthenticationServerPage : Gtk.Widget { 5 | hexpand: true; 6 | layout-manager: Gtk.BinLayout {}; 7 | 8 | Adw.ToastOverlay page_content { 9 | 10 | Adw.StatusPage { 11 | icon-name: "network-server-symbolic"; 12 | title: _("Set your Server"); 13 | description: _("Set the server to your Mastodon instance"); 14 | 15 | Adw.Clamp { 16 | maximum-size: 450; 17 | 18 | Gtk.ListBox { 19 | selection-mode: none; 20 | 21 | styles [ 22 | "boxed-list", 23 | ] 24 | 25 | Adw.EntryRow server_entry { 26 | title: _("Server Domain"); 27 | 28 | changed => on_input (); 29 | entry-activated => on_confirm (); 30 | 31 | Gtk.Button confirm_button { 32 | halign: center; 33 | valign: center; 34 | sensitive: false; 35 | 36 | clicked => on_confirm (); 37 | 38 | styles [ 39 | "circular", 40 | ] 41 | 42 | .WaitingButton button_waiting { 43 | icon-name: "go-next-symbolic"; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui/style-hc.css: -------------------------------------------------------------------------------- 1 | /* style-hc.css 2 | * 3 | * Copyright 2021 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | /** 22 | * Custom styles used by Cawbird when High Contrast is activated. 23 | */ 24 | 25 | /** 26 | * Adds an border around activated rows. 27 | */ 28 | list.session-list > row.activatable:hover, 29 | row.activatable:active, 30 | row.activatable:selected, 31 | listview.post-list > row.activatable:hover, 32 | row.activatable:active, 33 | row.activatable:selected { 34 | box-shadow: inset 0 0 0 1px @borders; 35 | } 36 | -------------------------------------------------------------------------------- /lib/Base/Content/UserEnums.vala: -------------------------------------------------------------------------------- 1 | /* UserEnums.vala 2 | * 3 | * Copyright 2021 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Flags defining some properties of a User. 25 | */ 26 | [Flags] 27 | public enum Backend.UserFlag { 28 | /** 29 | * Sets if the User moderates it’s followers. 30 | */ 31 | MODERATED, 32 | /** 33 | * Sets if the User don't allow public access to his timeline. 34 | */ 35 | PROTECTED, 36 | /** 37 | * Sets if the User is verified by the platform. 38 | */ 39 | VERIFIED, 40 | /** 41 | * Sets if the User is a automated bot. 42 | */ 43 | BOT 44 | } 45 | -------------------------------------------------------------------------------- /ui/Content/PostContent.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template PostContent : Gtk.Widget { 5 | layout-manager: Gtk.BinLayout {}; 6 | 7 | Gtk.Stack content_stack { 8 | transition-type: crossfade; 9 | 10 | Gtk.StackPage { 11 | name: "spoiler"; 12 | child: Gtk.Button { 13 | action-name: "post.toggle-sensitive"; 14 | 15 | styles [ 16 | "flat" 17 | ] 18 | 19 | Adw.StatusPage spoiler_description { 20 | styles [ 21 | "compact" 22 | ] 23 | 24 | description: _("Click to view content"); 25 | } 26 | }; 27 | } 28 | 29 | Gtk.StackPage { 30 | name: "content"; 31 | child: Gtk.Box content_box { 32 | orientation: vertical; 33 | spacing: 12; 34 | 35 | Gtk.Label text_label { 36 | xalign: 0.0; 37 | use-markup: true; 38 | wrap: true; 39 | wrap-mode: word_char; 40 | 41 | activate-link => on_link_clicked (); 42 | } 43 | 44 | .MediaPreview media_previewer {} 45 | 46 | Gtk.Button quote_button { 47 | styles [ 48 | "no-padding", 49 | "flat", 50 | "frame" 51 | ] 52 | } 53 | 54 | .PostMetrics post_metrics {} 55 | 56 | .PostActions post_actions {} 57 | }; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /ui/Media/MediaSelector.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template MediaSelector : Gtk.Button { 5 | overflow: hidden; 6 | 7 | styles [ 8 | "no-padding" 9 | ] 10 | 11 | Gtk.Overlay { 12 | Gtk.Spinner load_indicator { 13 | halign: center; 14 | valign: center; 15 | 16 | styles [ 17 | "large" 18 | ] 19 | } 20 | 21 | [overlay] 22 | .CroppedPicture media_holder {} 23 | 24 | [overlay] 25 | Adw.Bin { 26 | css-name: "button"; 27 | 28 | styles [ 29 | "flat" 30 | ] 31 | } 32 | 33 | [overlay] 34 | Gtk.Box media_indicator_box { 35 | visible: false; 36 | halign: end; 37 | valign: end; 38 | margin-end: 8; 39 | margin-bottom: 8; 40 | 41 | styles [ 42 | "osd", 43 | "indicator" 44 | ] 45 | 46 | Gtk.Image animated_type_indicator { 47 | visible: false; 48 | icon-name: "animated-symbolic"; 49 | tooltip-text: _("Animated GIF"); 50 | } 51 | 52 | Gtk.Image video_type_indicator { 53 | visible: false; 54 | icon-name: "camera-video-symbolic"; 55 | tooltip-text: _("Video"); 56 | } 57 | 58 | Gtk.Image alt_text_indicator { 59 | visible: false; 60 | icon-name: "accessories-text-editor-symbolic"; 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /lib/Base/Content/UserDataField.vala: -------------------------------------------------------------------------------- 1 | /* UserDataField.vala 2 | * 3 | * Copyright 2021-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * A field storing specific information about a User. 25 | */ 26 | public abstract class Backend.UserDataField : Object { 27 | 28 | /** 29 | * A description for this field. 30 | */ 31 | public string name { get; construct; } 32 | 33 | /** 34 | * The value of the data in this field. 35 | */ 36 | public string content { get; construct; } 37 | 38 | /** 39 | * The date at which the content of the field was verified by the server. 40 | */ 41 | public DateTime? verified { get; construct; } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/config.vapi: -------------------------------------------------------------------------------- 1 | /* config.vapi 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | /** 22 | * Configuration options set by meson. 23 | */ 24 | [CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "config.h")] 25 | namespace Config { 26 | 27 | /** 28 | * The name of the project. 29 | */ 30 | public const string PROJECT_NAME; 31 | 32 | /** 33 | * The version of the project. 34 | */ 35 | public const string PROJECT_VERSION; 36 | 37 | /** 38 | * The app id for the program. 39 | */ 40 | public const string APPLICATION_ID; 41 | 42 | /** 43 | * Where the localization files are located. 44 | */ 45 | public const string LOCALEDIR; 46 | } 47 | -------------------------------------------------------------------------------- /lib/Base/Collections/CollectionPins.vala: -------------------------------------------------------------------------------- 1 | /* CollectionPins.vala 2 | * 3 | * Copyright 2023 CodedOre <47981497+CodedOre@users.noreply.github.com> 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * An interface for Collection providing pinned posts. 25 | */ 26 | public interface Backend.CollectionPins : Backend.Collection { 27 | 28 | /** 29 | * Checks if an post in the collection was pinned by the user. 30 | * 31 | * If the post is not in this collection, the method returns false. 32 | * 33 | * @param post The post to check for. 34 | * 35 | * @return If the checked post was pinned by the user of this timeline. 36 | */ 37 | public abstract bool is_pinned_post (Post post); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ui/Preferences/SessionSettings.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template PreferencesSessionSettings : Gtk.Widget { 5 | layout-manager: Gtk.BoxLayout { 6 | orientation: vertical; 7 | }; 8 | 9 | Adw.HeaderBar page_header { 10 | [start] 11 | Gtk.Button { 12 | action-name: "preferences.close-subpage"; 13 | icon-name: "go-previous-symbolic"; 14 | } 15 | 16 | [title] 17 | Adw.WindowTitle page_title {} 18 | } 19 | 20 | Adw.Clamp account_settings_group { 21 | margin-top: 12; 22 | margin-bottom: 12; 23 | margin-start: 12; 24 | margin-end: 12; 25 | 26 | Adw.PreferencesGroup { 27 | title: _("Account Settings"); 28 | 29 | Adw.ActionRow { 30 | title: _("Auto-start"); 31 | activatable-widget: auto_start_switch; 32 | 33 | Gtk.Switch auto_start_switch { 34 | valign: center; 35 | halign: center; 36 | } 37 | } 38 | } 39 | } 40 | 41 | Adw.Clamp remove_account_group { 42 | margin-top: 12; 43 | margin-bottom: 12; 44 | margin-start: 12; 45 | margin-end: 12; 46 | 47 | Adw.PreferencesGroup { 48 | Adw.ActionRow { 49 | activatable: true; 50 | action-name: "session-settings.remove-session"; 51 | icon-name: "edit-delete-symbolic"; 52 | title: _("Remove Account"); 53 | 54 | Gtk.Image { 55 | icon-name: "go-next-symbolic"; 56 | } 57 | 58 | styles [ 59 | "destructive-action" 60 | ] 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /lib/Base/Collections/CollectionFilters.vala: -------------------------------------------------------------------------------- 1 | /* CollectionFilters.vala 2 | * 3 | * Copyright 2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * An interface for FilteredCollection providing 25 | * public properties for filters of an post collection. 26 | */ 27 | public interface Backend.PostFilters : Object { 28 | 29 | /** 30 | * If generic posts should be displayed. 31 | */ 32 | public abstract bool display_generic { get; set; } 33 | 34 | /** 35 | * If reposts should be displayed. 36 | */ 37 | public abstract bool display_reposts { get; set; } 38 | 39 | /** 40 | * If replies should be displayed. 41 | */ 42 | public abstract bool display_replies { get; set; } 43 | 44 | /** 45 | * If posts with media should be displayed. 46 | */ 47 | public abstract bool display_media { get; set; } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /ui/Authentication/AuthView.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template AuthView : Gtk.Widget { 5 | layout-manager: Gtk.BoxLayout { 6 | orientation: vertical; 7 | }; 8 | 9 | Adw.HeaderBar auth_header { 10 | show-start-title-buttons: false; 11 | show-end-title-buttons: false; 12 | 13 | styles [ 14 | "flat" 15 | ] 16 | 17 | [start] 18 | Gtk.Button back_button { 19 | label: _("Cancel"); 20 | clicked => back_button_action (); 21 | } 22 | 23 | [title] 24 | Adw.WindowTitle { 25 | title: _("Add Account"); 26 | } 27 | } 28 | 29 | Adw.Leaflet auth_leaflet { 30 | vexpand: true; 31 | can-unfold: false; 32 | can-navigate-back: true; 33 | transition-type: slide; 34 | notify::visible-child => on_change_page (); 35 | 36 | Adw.LeafletPage server_page { 37 | child: .AuthenticationServerPage { 38 | view: AuthView; 39 | }; 40 | } 41 | 42 | Adw.LeafletPage browser_page { 43 | child: .AuthenticationBrowserPage { 44 | view: AuthView; 45 | loader: load_widget; 46 | }; 47 | } 48 | 49 | Adw.LeafletPage code_page { 50 | child: .AuthenticationCodePage { 51 | view: AuthView; 52 | loader: load_widget; 53 | }; 54 | } 55 | 56 | Adw.LeafletPage load_page { 57 | child: .AuthenticationLoadPage load_widget { 58 | view: AuthView; 59 | }; 60 | } 61 | 62 | Adw.LeafletPage final_page { 63 | child: .AuthenticationFinalPage { 64 | view: AuthView; 65 | }; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /lib/Mastodon/Utils/ParseUtils.vala: -------------------------------------------------------------------------------- 1 | /* ParseUtils.vala 2 | * 3 | * Copyright 2022 CodedOre <47981497+CodedOre@users.noreply.github.com> 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Provides utilities used parsing the json of content. 25 | */ 26 | namespace Backend.Mastodon.Utils.ParseUtils { 27 | 28 | /** 29 | * Get the domain from a url. 30 | * 31 | * @param url The url for the domain. 32 | * 33 | * @return The domain from the url. 34 | */ 35 | private string strip_domain (string url) { 36 | // Run a Regex to get the domain 37 | try { 38 | var regex = new Regex ("https?://(www.)?(.*?)/.*"); 39 | return regex.replace ( 40 | url, 41 | url.length, 42 | 0, 43 | "\\2" 44 | ); 45 | } catch (RegexError e) { 46 | error (@"Error while parsing domain: $(e.message)"); 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /tests/Parsing/UserData/Mastodon/BasicUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "106866867257179166", 3 | "username": "cawbird_test_account", 4 | "acct": "cawbird_test_account", 5 | "display_name": "CawbirdTestAccount", 6 | "locked": false, 7 | "bot": false, 8 | "discoverable": false, 9 | "group": false, 10 | "created_at": "2021-09-03T00:00:00.000Z", 11 | "note": "

This may or may not be a account from @CodedOre for testing some API's for a certain Client.

", 12 | "url": "https://mastodon.social/@cawbird_test_account", 13 | "avatar": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png", 14 | "avatar_static": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png", 15 | "header": "https://files.mastodon.social/accounts/headers/106/866/867/257/179/166/original/1b43374005561eec.jpeg", 16 | "header_static": "https://files.mastodon.social/accounts/headers/106/866/867/257/179/166/original/1b43374005561eec.jpeg", 17 | "followers_count": 3, 18 | "following_count": 0, 19 | "statuses_count": 11, 20 | "last_status_at": "2021-12-20", 21 | "emojis": [], 22 | "fields": [ 23 | { 24 | "name": "Lives in", 25 | "value": "org.gnome.Sdk", 26 | "verified_at": null 27 | }, 28 | { 29 | "name": "Example Weblink", 30 | "value": "https://example.com", 31 | "verified_at": null 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /ui/meson.build: -------------------------------------------------------------------------------- 1 | # Cawbird UI build file 2 | 3 | # Compile Blueprint files 4 | blueprints = custom_target( 5 | 'blueprints', 6 | input: files( 7 | 'MainWindow.blp', 8 | 'Authentication/AuthView.blp', 9 | 'Authentication/BrowserPage.blp', 10 | 'Authentication/CodePage.blp', 11 | 'Authentication/FinalPage.blp', 12 | 'Authentication/LoadPage.blp', 13 | 'Authentication/ServerPage.blp', 14 | 'Authentication/StartPage.blp', 15 | 'Collections/CollectionFilter.blp', 16 | 'Collections/CollectionView.blp', 17 | 'Content/PostActions.blp', 18 | 'Content/PostContent.blp', 19 | 'Content/PostItem.blp', 20 | 'Content/PostMetrics.blp', 21 | 'Content/PostStatus.blp', 22 | 'Content/UserCard.blp', 23 | 'Content/UserDataDisplay.blp', 24 | 'Content/UserDisplay.blp', 25 | 'Media/MediaDialog.blp', 26 | 'Media/MediaDisplay.blp', 27 | 'Media/MediaDisplayItem.blp', 28 | 'Media/MediaSelector.blp', 29 | 'Pages/MainPage.blp', 30 | 'Pages/ThreadPage.blp', 31 | 'Pages/UserPage.blp', 32 | 'Preferences/AppearancesPage.blp', 33 | 'Preferences/PreferencesWindow.blp', 34 | 'Preferences/SessionSettings.blp', 35 | 'Preferences/SessionsPage.blp', 36 | 'Widgets/BadgesBox.blp', 37 | 'Widgets/FilterButton.blp', 38 | 'Widgets/SessionRow.blp', 39 | 'Widgets/SessionSidebar.blp', 40 | 'Widgets/UserAvatar.blp', 41 | 'Widgets/UserButton.blp', 42 | 'Widgets/WaitingButton.blp' 43 | ), 44 | output: '.', 45 | command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], 46 | ) 47 | 48 | # Install UI resources 49 | cawresources += gnome.compile_resources( 50 | 'cawbird_ui_resources', 51 | 'interface.gresource.xml', 52 | dependencies: blueprints, 53 | c_name: 'cawbird_ui' 54 | ) 55 | -------------------------------------------------------------------------------- /.github/workflows/flatpak.yml: -------------------------------------------------------------------------------- 1 | # Build a development Flatpak build 2 | name: Build Flatpak 3 | on: 4 | # Runs on push in repository or pull requests 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | flatpak: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: bilelmoussaoui/flatpak-github-actions:gnome-nightly 13 | options: --privileged 14 | # We run the job three times with different platforms enabled 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | # Disable "full" build, because it is currently identical to "Mastodon" 19 | #platforms: [Mastodon, Full] 20 | platforms: [Mastodon] 21 | arch: [x86_64, aarch64] 22 | name: "${{ matrix.platforms }} Build - ${{ matrix.arch }}" 23 | env: 24 | BACKENDS: ${{ matrix.platforms }} 25 | steps: 26 | - name: Checkout the repository 27 | uses: actions/checkout@v3 28 | with: 29 | submodules: true 30 | # Installing docker for setup-qemu-action 31 | - name: Install deps 32 | run: | 33 | dnf -y install docker 34 | # Using QEMU to cross-compile for ARM 35 | - name: Set up QEMU 36 | id: qemu 37 | uses: docker/setup-qemu-action@v2 38 | with: 39 | platforms: arm64 40 | - name: Run Flatpak Builder 41 | uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v5 42 | with: 43 | arch: ${{ matrix.arch }} 44 | bundle: "Cawbird-Devel-${{ matrix.platforms }}.flatpak" 45 | manifest-path: build-aux/Flatpak/uk.co.ibboard.Cawbird.Devel.json 46 | repository-name: gnome-nightly 47 | repository-url: https://nightly.gnome.org/gnome-nightly.flatpakrepo 48 | run-tests: true 49 | cache-key: flatpak-builder-${{ matrix.platforms }}-${{ github.sha }} 50 | -------------------------------------------------------------------------------- /lib/Base/Content/TextModule.vala: -------------------------------------------------------------------------------- 1 | /* TextModule.vala 2 | * 3 | * Copyright 2021-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * The type of content a TextModules stores. 25 | * 26 | * Used when formatting the text for the UI. 27 | */ 28 | public enum Backend.TextModuleType { 29 | TEXT, 30 | TAG, 31 | TRAIL_TAG, 32 | MENTION, 33 | WEBLINK 34 | } 35 | 36 | /** 37 | * Stores a part of an text in a Post. 38 | * 39 | * A text in an Post is internally split into multiple TextModules 40 | * which store parts of different type, as seen in TextModuleType. 41 | */ 42 | public struct Backend.TextModule { 43 | 44 | /** 45 | * What content this module stores. 46 | */ 47 | public TextModuleType type; 48 | 49 | /** 50 | * The text contained in this module. 51 | */ 52 | public string display; 53 | 54 | /** 55 | * The target link of the module, used as tooltip and link. 56 | */ 57 | public string? target; 58 | 59 | /** 60 | * The start position in the text. 61 | */ 62 | public uint text_start; 63 | 64 | /** 65 | * The end position in the text. 66 | */ 67 | public uint text_end; 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Widgets/FilterButton.vala: -------------------------------------------------------------------------------- 1 | /* FilterButton.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * A button inside the CollectionFilter widget. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Widgets/FilterButton.ui")] 27 | public class FilterButton : Gtk.Widget { 28 | 29 | // UI-Elements of FilterButton 30 | [GtkChild] 31 | private unowned Gtk.ToggleButton button; 32 | 33 | /** 34 | * The label of this button. 35 | */ 36 | public string label { get; set; } 37 | 38 | /** 39 | * If this filter is selected. 40 | */ 41 | public bool active { get; set; } 42 | 43 | /** 44 | * Activated when the button is toggled. 45 | */ 46 | public signal void toggled (); 47 | 48 | /** 49 | * Runs when the internal button is clicked. 50 | */ 51 | [GtkCallback] 52 | private void on_toggled () { 53 | this.active = button.active; 54 | toggled (); 55 | } 56 | 57 | /** 58 | * Deconstructs FilterButton and it's childrens. 59 | */ 60 | public override void dispose () { 61 | // Destructs children of FilterButton 62 | button.unparent (); 63 | base.dispose (); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/Authentication/FinalPage.vala: -------------------------------------------------------------------------------- 1 | /* FinalPage.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * The page showing the authentication success. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Authentication/FinalPage.ui")] 27 | public class Authentication.FinalPage : Gtk.Widget { 28 | 29 | // UI-Elements of FinalPage 30 | [GtkChild] 31 | private unowned Adw.StatusPage page_content; 32 | 33 | /** 34 | * The AuthView holding this page. 35 | */ 36 | public weak AuthView view { get; construct; } 37 | 38 | /** 39 | * Run at construction of the widget. 40 | */ 41 | construct { 42 | // Check if children of AuthView 43 | if (view == null) { 44 | critical ("Can only be children to AuthView!"); 45 | } 46 | } 47 | 48 | /** 49 | * Finalize the authentication. 50 | */ 51 | [GtkCallback] 52 | private void on_continue () { 53 | view.close_auth (); 54 | } 55 | 56 | /** 57 | * Deconstructs FinalPage and it's childrens. 58 | */ 59 | public override void dispose () { 60 | // Deconstruct childrens 61 | page_content.unparent (); 62 | base.dispose (); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /ui/Pages/MainPage.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template MainPage : Gtk.Widget { 5 | layout-manager: Gtk.BinLayout {}; 6 | 7 | Adw.Flap page_flap { 8 | fold-policy: always; 9 | reveal-flap: bind content_sidebar_toggle.active; 10 | 11 | [content] 12 | Gtk.Box { 13 | orientation: vertical; 14 | 15 | Adw.HeaderBar { 16 | show-start-title-buttons: bind page_flap.folded; 17 | 18 | [start] 19 | Gtk.ToggleButton content_sidebar_toggle { 20 | icon-name: "sidebar-show-symbolic"; 21 | active: bind flap_sidebar_toggle.active; 22 | } 23 | 24 | [title] 25 | Adw.WindowTitle content_title {} 26 | 27 | [end] 28 | Gtk.MenuButton { 29 | icon-name: "open-menu-symbolic"; 30 | menu-model: main-menu; 31 | primary: true; 32 | } 33 | } 34 | 35 | .CollectionView home_collection { 36 | vexpand: true; 37 | } 38 | } 39 | 40 | [separator] 41 | Gtk.Separator { 42 | orientation: vertical; 43 | } 44 | 45 | [flap] 46 | Gtk.Box { 47 | orientation: vertical; 48 | width-request: 300; 49 | 50 | styles [ 51 | "background" 52 | ] 53 | 54 | Adw.HeaderBar { 55 | show-start-title-buttons: bind page_flap.folded inverted; 56 | show-end-title-buttons: false; 57 | 58 | [start] 59 | Gtk.ToggleButton flap_sidebar_toggle { 60 | icon-name: "sidebar-show-symbolic"; 61 | active: bind page_flap.reveal-flap; 62 | visible: bind page_flap.folded; 63 | } 64 | 65 | [title] 66 | Adw.WindowTitle flap_title {} 67 | } 68 | 69 | .SessionSidebar session_sidebar {} 70 | } 71 | } 72 | } 73 | 74 | menu main-menu { 75 | item { 76 | label: _("Preferences"); 77 | action: "app.preferences"; 78 | } 79 | item { 80 | label: _("About"); 81 | action: "app.about"; 82 | } 83 | } -------------------------------------------------------------------------------- /ui/interface.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | style.css 6 | style-dark.css 7 | style-hc.css 8 | 9 | 10 | 11 | MainWindow.ui 12 | Authentication/AuthView.ui 13 | Authentication/BrowserPage.ui 14 | Authentication/CodePage.ui 15 | Authentication/FinalPage.ui 16 | Authentication/LoadPage.ui 17 | Authentication/ServerPage.ui 18 | Collections/CollectionFilter.ui 19 | Collections/CollectionView.ui 20 | Content/PostActions.ui 21 | Content/PostContent.ui 22 | Content/PostItem.ui 23 | Content/PostMetrics.ui 24 | Content/PostStatus.ui 25 | Content/UserCard.ui 26 | Content/UserDataDisplay.ui 27 | Content/UserDisplay.ui 28 | Media/MediaDialog.ui 29 | Media/MediaDisplay.ui 30 | Media/MediaDisplayItem.ui 31 | Media/MediaSelector.ui 32 | Pages/MainPage.ui 33 | Pages/ThreadPage.ui 34 | Pages/UserPage.ui 35 | Preferences/AppearancesPage.ui 36 | Preferences/PreferencesWindow.ui 37 | Preferences/SessionSettings.ui 38 | Preferences/SessionsPage.ui 39 | Widgets/BadgesBox.ui 40 | Widgets/FilterButton.ui 41 | Widgets/SessionRow.ui 42 | Widgets/SessionSidebar.ui 43 | Widgets/UserAvatar.ui 44 | Widgets/UserButton.ui 45 | Widgets/WaitingButton.ui 46 | 47 | 48 | -------------------------------------------------------------------------------- /ui/Content/PostItem.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template PostItem : Gtk.Widget { 5 | margin-start: 12; 6 | margin-end: 12; 7 | layout-manager: Gtk.BoxLayout { 8 | orientation: vertical; 9 | }; 10 | 11 | Gtk.Box pinned_status { 12 | visible: bind PostItem.pinned-item; 13 | spacing: 8; 14 | 15 | Adw.Bin { 16 | width-request: 48; 17 | height-request: 12; 18 | halign: start; 19 | 20 | Gtk.Separator { 21 | visible: bind PostItem.connect-to-previous; 22 | halign: center; 23 | orientation: vertical; 24 | } 25 | } 26 | 27 | Gtk.Image { 28 | icon-name: "view-pin"; 29 | margin-top: 12; 30 | 31 | styles [ 32 | "dim-label" 33 | ] 34 | } 35 | 36 | Gtk.Label pinned_label { 37 | margin-top: 12; 38 | 39 | styles [ 40 | "caption", 41 | "dim-label" 42 | ] 43 | } 44 | } 45 | 46 | .PostStatus repost_status { 47 | show-previous: bind PostItem.connect-to-previous; 48 | display-inline: true; 49 | show-time: true; 50 | is-repost: true; 51 | } 52 | 53 | .PostStatus post_status {} 54 | 55 | Gtk.Box content_box { 56 | spacing: 8; 57 | 58 | Adw.Bin next_line_bin { 59 | width-request: 48; 60 | 61 | Gtk.Separator next_line { 62 | visible: bind PostItem.connect-to-next; 63 | halign: center; 64 | orientation: vertical; 65 | } 66 | } 67 | 68 | Gtk.Box { 69 | orientation: vertical; 70 | margin-bottom: 12; 71 | margin-start: 10; 72 | margin-end: 4; 73 | spacing: 8; 74 | 75 | Gtk.Label info_label { 76 | use-markup: true; 77 | xalign: 0.0; 78 | 79 | styles [ 80 | "caption", 81 | "dim-label" 82 | ] 83 | } 84 | 85 | .PostContent post_content { 86 | hexpand: true; 87 | display-mode: bind PostItem.display-mode; 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /tests/Parsing/MediaData/Mastodon/OnePictureChecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "107041808020442008", 3 | "post_type": "BACKEND_POST_TYPE_NORMAL", 4 | "creation_date": "2021-10-04T06:19:29.462Z", 5 | "text": { 6 | "no_flags": "A new post is there!\nLet's test one picture here:" 7 | }, 8 | "text_modules": [ 9 | { 10 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 11 | "display": "A new post is there!", 12 | "target": null, 13 | "text_start": 0, 14 | "text_end": 20 15 | }, 16 | { 17 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 18 | "display": "\n", 19 | "target": null, 20 | "text_start": 20, 21 | "text_end": 21 22 | }, 23 | { 24 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 25 | "display": "Let's test one picture here:", 26 | "target": null, 27 | "text_start": 21, 28 | "text_end": 49 29 | } 30 | ], 31 | "author": { 32 | "id": "106866867257179166", 33 | "display_name": "CawbirdTestAccount", 34 | "username": "cawbird_test_account", 35 | "avatar_url": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png" 36 | }, 37 | "attached_media": [ 38 | { 39 | "id": "107041799746036809", 40 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 41 | "alt_text": "The default background of the GNOME 41 desktop.", 42 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/041/799/746/036/809/small/46b41f9d41fcdaf3.png", 43 | "media_url": "https://files.mastodon.social/media_attachments/files/107/041/799/746/036/809/original/46b41f9d41fcdaf3.png", 44 | "width": 1983, 45 | "height": 1046 46 | } 47 | ], 48 | "domain": "mastodon.social", 49 | "url": "https://mastodon.social/@cawbird_test_account/107041808020442008", 50 | "source": "Web", 51 | "liked_count": 0, 52 | "replied_count": 0, 53 | "reposted_count": 0 54 | } 55 | -------------------------------------------------------------------------------- /lib/Base/Content/PostAuxiliary.vala: -------------------------------------------------------------------------------- 1 | /* PostAuxiliary.vala 2 | * 3 | * Copyright 2021-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Different types for a Post. 25 | * 26 | * Determines some settings when displaying this particular post. 27 | */ 28 | public enum Backend.PostType { 29 | /** 30 | * Your normal day-to-day post. 31 | */ 32 | NORMAL, 33 | /** 34 | * A repost from an different post. 35 | */ 36 | REPOST, 37 | /** 38 | * A repost with additional content added. 39 | */ 40 | QUOTE 41 | } 42 | 43 | /** 44 | * The sensitivity of the content of a post. 45 | * 46 | * Determines what of the content should be displayed directly. 47 | */ 48 | public enum Backend.PostSensitivity { 49 | /** 50 | * Nothing has to be hidden. 51 | */ 52 | NONE, 53 | /** 54 | * Only the media is sensitive. 55 | */ 56 | MEDIA, 57 | /** 58 | * All content is sensitive. 59 | */ 60 | ALL 61 | } 62 | 63 | /** 64 | * Stores interaction data to update a post with. 65 | * 66 | * Used by Session to update the data of an post when an 67 | * API call altered the interaction data values. 68 | */ 69 | internal struct Backend.PostInteractionData { 70 | int liked_count; 71 | int replied_count; 72 | int reposted_count; 73 | bool is_favourited; 74 | bool is_reposted; 75 | } 76 | -------------------------------------------------------------------------------- /tests/Parsing/PostData/Mastodon/BasicChecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "106883926076492274", 3 | "post_type": "BACKEND_POST_TYPE_NORMAL", 4 | "creation_date": "2021-09-06T09:08:02.188Z", 5 | "text": { 6 | "no_flags": "Hello World!\n\nThis is a basic post with some text in it. Nothing more...\n\nI mean, this is a test profil after all, so don't expect an interesting text." 7 | }, 8 | "text_modules": [ 9 | { 10 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 11 | "display": "Hello World!", 12 | "target": null, 13 | "text_start": 0, 14 | "text_end": 12 15 | }, 16 | { 17 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 18 | "display": "\n\n", 19 | "target": null, 20 | "text_start": 12, 21 | "text_end": 14 22 | }, 23 | { 24 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 25 | "display": "This is a basic post with some text in it. Nothing more...", 26 | "target": null, 27 | "text_start": 14, 28 | "text_end": 72 29 | }, 30 | { 31 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 32 | "display": "\n\n", 33 | "target": null, 34 | "text_start": 72, 35 | "text_end": 74 36 | }, 37 | { 38 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 39 | "display": "I mean, this is a test profil after all, so don't expect an interesting text.", 40 | "target": null, 41 | "text_start": 74, 42 | "text_end": 151 43 | } 44 | ], 45 | "author": { 46 | "id": "106866867257179166", 47 | "display_name": "CawbirdTestAccount", 48 | "username": "cawbird_test_account", 49 | "avatar_url": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png" 50 | }, 51 | "domain": "mastodon.social", 52 | "url": "https://mastodon.social/@cawbird_test_account/106883926076492274", 53 | "source": "Web", 54 | "liked_count": 0, 55 | "replied_count": 0, 56 | "reposted_count": 1 57 | } 58 | -------------------------------------------------------------------------------- /data/icons/scalable/actions/repost-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 36 | 39 | 40 | 43 | 47 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/Parsing/UserData/Mastodon/BasicChecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "106866867257179166", 3 | "display_name": "CawbirdTestAccount", 4 | "username": "cawbird_test_account", 5 | "avatar_url": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png", 6 | "header_url": "https://files.mastodon.social/accounts/headers/106/866/867/257/179/166/original/1b43374005561eec.jpeg", 7 | "flags": { 8 | "verified": false, 9 | "moderated": false, 10 | "protected": false, 11 | "bot": false 12 | }, 13 | "creation_date": "2021-09-03T00:00:00.000Z", 14 | "url": "https://mastodon.social/@cawbird_test_account", 15 | "domain": "mastodon.social", 16 | "followers_count": 3, 17 | "following_count": 0, 18 | "posts_count": 11, 19 | "description": "This may or may not be a account from @CodedOre for testing some API's for a certain Client.", 20 | "description_modules": [ 21 | { 22 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 23 | "display": "This may or may not be a account from ", 24 | "target": null, 25 | "text_start": 0, 26 | "text_end": 38 27 | }, 28 | { 29 | "type": "BACKEND_TEXT_MODULE_TYPE_MENTION", 30 | "display": "@CodedOre", 31 | "target": "CodedOre@mastodon.online", 32 | "text_start": 38, 33 | "text_end": 47 34 | }, 35 | { 36 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 37 | "display": " for testing some API's for a certain Client.", 38 | "target": null, 39 | "text_start": 47, 40 | "text_end": 92 41 | } 42 | ], 43 | "data_fields": [ 44 | { 45 | "type": "BACKEND_USER_DATA_FIELD_TYPE_GENERIC", 46 | "name": "Lives in", 47 | "display": "org.gnome.Sdk", 48 | "target": null 49 | }, 50 | { 51 | "type": "BACKEND_USER_DATA_FIELD_TYPE_GENERIC", 52 | "name": "Example Weblink", 53 | "display": "example.com", 54 | "target": "https://example.com" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /data/icons/scalable/actions/platform-mastodon-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Preferences/AppearancesPage.vala: -------------------------------------------------------------------------------- 1 | /* AppearancesPage.vala 2 | * 3 | * Copyright 2022-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Displays the page regarding the appearance options. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Preferences/AppearancesPage.ui")] 27 | public class Preferences.AppearancesPage : Adw.PreferencesPage { 28 | 29 | // UI-Elements of AppearancesPage 30 | [GtkChild] 31 | private unowned PostItem example_post_item; 32 | [GtkChild] 33 | private unowned Gtk.Switch round_avatar_switch; 34 | [GtkChild] 35 | private unowned Gtk.Switch trailing_tags_switch; 36 | [GtkChild] 37 | private unowned Gtk.Switch double_click_activation_switch; 38 | 39 | /** 40 | * Run at construction of the page. 41 | */ 42 | construct { 43 | // Bind the settings to the preferences widget 44 | var settings = new Settings ("uk.co.ibboard.Cawbird"); 45 | settings.bind ("round-avatars", 46 | round_avatar_switch, "active", 47 | GLib.SettingsBindFlags.DEFAULT); 48 | settings.bind ("trailing-tags", 49 | trailing_tags_switch, "active", 50 | GLib.SettingsBindFlags.DEFAULT); 51 | settings.bind ("double-click-activation", 52 | double_click_activation_switch, "active", 53 | GLib.SettingsBindFlags.DEFAULT); 54 | 55 | // Set up the example post 56 | example_post_item.post = new Backend.Utils.ExamplePost (); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'Cawbird', 3 | ['vala', 'c'], 4 | version: '1.9.0', 5 | meson_version: '>= 0.59.0', 6 | ) 7 | add_project_arguments('-DG_LOG_DOMAIN="cawbird"', language: 'c') 8 | 9 | # Load meson modules 10 | gnome = import('gnome') 11 | i18n = import('i18n') 12 | 13 | # Set up translation domain 14 | add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (meson.project_name()), language:'c') 15 | 16 | # Set build configuration 17 | dev_build = get_option('buildtype') == 'debug' or get_option('buildtype') == 'debugoptimized' 18 | if dev_build 19 | add_project_arguments('-D', 'DEBUG', language: 'vala') 20 | application_id = 'uk.co.ibboard.Cawbird.Devel' 21 | 22 | # Create a version number based on the git commit for development versions 23 | git = find_program('git', required : false, disabler : true) 24 | if git.found() 25 | # Get short version of commit id and use it as version number 26 | git_branch = run_command(git, 'rev-parse', '--abbrev-ref', 'HEAD', check: true) 27 | git_commit = run_command(git, 'rev-parse', '--short', 'HEAD', check: true) 28 | version = git_branch.stdout().strip() + '-' + git_commit.stdout().strip() 29 | else 30 | # Use project version as fallback 31 | version = meson.project_version() + '-devel' 32 | endif 33 | else 34 | # Use project version in release builds 35 | application_id = 'uk.co.ibboard.Cawbird' 36 | version = meson.project_version() 37 | endif 38 | 39 | message('Building ' + meson.project_name () + ' ' + version) 40 | 41 | # Configure Backends 42 | mastodon_backend = get_option('backends') == 'Mastodon' or get_option('backends') == 'Full' 43 | 44 | # Configure Mastodon platform 45 | if mastodon_backend 46 | add_project_arguments('-D', 'SUPPORT_MASTODON', language: 'vala') 47 | endif 48 | 49 | # Build and test the backend from subdirectories 50 | subdir('lib') 51 | # FIXME: We need new tests 52 | # subdir('tests') 53 | 54 | # Build the code and data for the client 55 | cawresources = [] 56 | subdir('data') 57 | subdir('ui') 58 | subdir('src') 59 | 60 | # Add the translations 61 | subproject('po') 62 | 63 | # Run post-install actions 64 | gnome.post_install( 65 | glib_compile_schemas: true, 66 | gtk_update_icon_cache: true, 67 | update_desktop_database: true 68 | ) -------------------------------------------------------------------------------- /data/uk.co.ibboard.Cawbird.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @app_id@ 6 | cawbird 7 | @app_id@.desktop 8 | 9 | cawbird 10 | 11 | 12 | 13 | GPL-3.0+ 14 | CC0-1.0 15 | 16 | 17 | 18 | @app_name@ 19 | Your window to the social world 20 | 21 |

Cawbird is a client for the short-text social networks like Mastodon.

22 |
23 | 24 | 25 | IBBoard 26 | 27 | 28 | https://ibboard.co.uk/cawbird/ 29 | https://github.com/ibboard/cawbird/issues 30 | https://www.transifex.com/cawbird/cawbird/ 31 | 32 | 33 | 34 | 360 35 | 36 | 37 | keyboard 38 | pointing 39 | touch 40 | 41 | 42 | 43 | 44 | moderate 45 | mild 46 | intense 47 | 48 | 49 | 50 | 51 | 52 | https://ibboard.co.uk/cawbird/appdata/screenshot4.jpg 53 | Screenshot caption 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |

This is a development build for the upcoming 2.0 release of Cawbird!

62 |
63 |
64 |
65 |
66 | -------------------------------------------------------------------------------- /tests/Parsing/UserParsing.vala: -------------------------------------------------------------------------------- 1 | /* UserParsing.vala 2 | * 3 | * Copyright 2021-2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Tests creation of a specific user and runs test on it. 25 | */ 26 | void run_user_test (string module, string user_json, string check_json) { 27 | Json.Object check_object; 28 | Json.Object user_object; 29 | Backend.User checked_user; 30 | 31 | // Creates a User object from the user json 32 | check_object = TestUtils.load_json (@"UserData/$(module)/$(check_json)"); 33 | user_object = TestUtils.load_json (@"UserData/$(module)/$(user_json)"); 34 | switch (module) { 35 | #if SUPPORT_MASTODON 36 | case "Mastodon": 37 | checked_user = Backend.Mastodon.User.from_json (user_object); 38 | break; 39 | #endif 40 | default: 41 | error ("No valid User could be created!"); 42 | } 43 | 44 | // Check parsed user against check objects. 45 | UserChecks.check_basic_fields (checked_user, check_object); 46 | UserChecks.check_additional_fields (checked_user, check_object); 47 | #if DEBUG 48 | UserChecks.check_description_parsing (checked_user, check_object); 49 | #endif 50 | UserChecks.check_data_fields (checked_user, check_object); 51 | } 52 | 53 | /** 54 | * Tests parsing of User content. 55 | */ 56 | int main (string[] args) { 57 | Test.init (ref args); 58 | 59 | #if SUPPORT_MASTODON 60 | Test.add_func ("/UserParsing/BasicUser/Mastodon", () => { 61 | run_user_test ("Mastodon", "BasicUser.json", "BasicChecks.json"); 62 | }); 63 | #endif 64 | 65 | Test.set_nonfatal_assertions (); 66 | return Test.run (); 67 | } 68 | -------------------------------------------------------------------------------- /src/Preferences/PreferencesWindow.vala: -------------------------------------------------------------------------------- 1 | /* PreferencesWindow.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Display settable preferences to the user. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Preferences/PreferencesWindow.ui")] 27 | public class PreferencesWindow : Adw.PreferencesWindow { 28 | 29 | #if DEBUG 30 | /** 31 | * Run at construction of an window. 32 | */ 33 | construct { 34 | // Add development style in debug 35 | add_css_class ("devel"); 36 | } 37 | #endif 38 | 39 | /** 40 | * Run at initialization of the class. 41 | */ 42 | class construct { 43 | // Set up a action to close a sub-page 44 | install_action ("preferences.close-subpage", null, (widget, action) => { 45 | var window = widget as PreferencesWindow; 46 | window.close_subpage (); 47 | }); 48 | // Set up the session actions 49 | install_action ("preferences.add-session", null, (widget, action) => { 50 | var window = widget as PreferencesWindow; 51 | if (window != null) { 52 | var auth_view = new AuthView (); 53 | window.present_subpage (auth_view); 54 | auth_view.close_auth.connect (() => { 55 | window.close_subpage (); 56 | }); 57 | } 58 | }); 59 | } 60 | 61 | /** 62 | * Displays an Session in a SessionSettings subview. 63 | * 64 | * @param session The session to be displayed. 65 | */ 66 | public void display_session_settings (Backend.Session session) { 67 | var settings_view = new Preferences.SessionSettings (); 68 | settings_view.session = session; 69 | this.present_subpage (settings_view); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Authentication/StartPage.vala: -------------------------------------------------------------------------------- 1 | /* StartPage.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * The first page for the authentication process. 25 | * 26 | * Currently unused because we now only support Mastodon 27 | */ 28 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Authentication/StartPage.ui")] 29 | public class Authentication.StartPage : Gtk.Widget { 30 | 31 | // UI-Elements of StartPage 32 | [GtkChild] 33 | private unowned Adw.StatusPage page_content; 34 | [GtkChild] 35 | private unowned Gtk.Button mastodon_button; 36 | 37 | /** 38 | * The AuthView holding this page. 39 | */ 40 | public weak AuthView view { get; construct; } 41 | 42 | /** 43 | * Run at construction of the widget. 44 | */ 45 | construct { 46 | // Check if children of AuthView 47 | if (view == null) { 48 | critical ("Can only be children to AuthView!"); 49 | } 50 | 51 | #if SUPPORT_MASTODON 52 | // Enable the Mastodon login button 53 | mastodon_button.visible = true; 54 | mastodon_button.clicked.connect (begin_mastodon_auth); 55 | #endif 56 | } 57 | 58 | /** 59 | * Activated when back button is activated. 60 | */ 61 | public void on_back_action () { 62 | } 63 | 64 | #if SUPPORT_MASTODON 65 | /** 66 | * Begins the Mastodon authentication. 67 | */ 68 | private void begin_mastodon_auth () { 69 | // Move to server page 70 | view.move_to_next (); 71 | } 72 | #endif 73 | 74 | /** 75 | * Deconstructs StartPage and it's childrens. 76 | */ 77 | public override void dispose () { 78 | // Deconstruct childrens 79 | page_content.unparent (); 80 | base.dispose (); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | # Cawbird data build file 2 | 3 | # Add an "Development Version" on debug builds 4 | desktop_name = meson.project_name () 5 | if dev_build 6 | desktop_name += ' (Development Version)' 7 | endif 8 | 9 | # Configure application informations 10 | application_info = configuration_data() 11 | application_info.set('app_id', application_id) 12 | application_info.set('app_name', desktop_name) 13 | 14 | # Configure Desktop file 15 | desktop_file_config = configure_file( 16 | input: 'uk.co.ibboard.Cawbird.desktop', 17 | output: '@0@.desktop.in'.format(application_id), 18 | configuration: application_info 19 | ) 20 | 21 | # Install translated Desktop file 22 | desktop_file = i18n.merge_file( 23 | input: desktop_file_config, 24 | output: ('@0@.desktop').format(application_id), 25 | type: 'desktop', 26 | po_dir: '../subprojects/po', 27 | install: true, 28 | install_dir: join_paths(get_option('datadir'), 'applications') 29 | ) 30 | 31 | desktop_utils = find_program('desktop-file-validate', required: false) 32 | if desktop_utils.found() 33 | test('Validate desktop file', desktop_utils, 34 | args: [desktop_file] 35 | ) 36 | endif 37 | 38 | # Configure Appstream file 39 | appstream_file_config = configure_file( 40 | input: 'uk.co.ibboard.Cawbird.appdata.xml', 41 | output: '@0@.appdata.xml.in'.format(application_id), 42 | configuration: application_info 43 | ) 44 | 45 | # Install translated Appstream file 46 | appstream_file = i18n.merge_file( 47 | input: appstream_file_config, 48 | output: ('@0@.appdata.xml').format(application_id), 49 | po_dir: '../subprojects/po', 50 | install: true, 51 | install_dir: join_paths(get_option('datadir'), 'appdata') 52 | ) 53 | 54 | appstream_util = find_program('appstream-util', required: false) 55 | if appstream_util.found() 56 | test('Validate appstream file', appstream_util, 57 | args: ['validate', appstream_file] 58 | ) 59 | endif 60 | 61 | # Install GSettings schema 62 | install_data('uk.co.ibboard.Cawbird.gschema.xml', 63 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 64 | ) 65 | 66 | compile_schemas = find_program('glib-compile-schemas', required: false) 67 | if compile_schemas.found() 68 | test('Validate schema file', compile_schemas, 69 | args: ['--strict', '--dry-run', meson.current_source_dir()] 70 | ) 71 | endif 72 | 73 | # Install icons for Cawbird 74 | subdir('icons') 75 | -------------------------------------------------------------------------------- /src/Widgets/BadgesBox.vala: -------------------------------------------------------------------------------- 1 | /* BadgesBox.vala 2 | * 3 | * Copyright 2021 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * A small helper widget to display different badges. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Widgets/BadgesBox.ui")] 27 | public class BadgesBox : Gtk.Box { 28 | 29 | /** 30 | * The size for all used icons. 31 | */ 32 | public int icon_size { get; set; default = 10; } 33 | 34 | /** 35 | * If the verified badge should be shown. 36 | */ 37 | public bool display_verified { 38 | get { 39 | return verified_visible; 40 | } 41 | set { 42 | verified_visible = value; 43 | this.visible = verified_visible || bot_visible || protected_visible; 44 | } 45 | } 46 | 47 | /** 48 | * If the bot badge should be shown. 49 | */ 50 | public bool display_bot { 51 | get { 52 | return bot_visible; 53 | } 54 | set { 55 | bot_visible = value; 56 | this.visible = verified_visible || bot_visible || protected_visible; 57 | } 58 | } 59 | 60 | /** 61 | * If the protected badge should be shown. 62 | */ 63 | public bool display_protected { 64 | get { 65 | return protected_visible; 66 | } 67 | set { 68 | protected_visible = value; 69 | this.visible = verified_visible || bot_visible || protected_visible; 70 | } 71 | } 72 | 73 | /** 74 | * Stores if the verified badge should be shown. 75 | */ 76 | private bool verified_visible = false; 77 | 78 | /** 79 | * Stores if the bot badge should be shown. 80 | */ 81 | private bool bot_visible = false; 82 | 83 | /** 84 | * Stores if the protected badge should be shown. 85 | */ 86 | private bool protected_visible = false; 87 | 88 | } 89 | -------------------------------------------------------------------------------- /data/icons/scalable/apps/uk.co.ibboard.Cawbird.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/Parsing/MediaData/Mastodon/TwoPictureChecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "107099804533083877", 3 | "post_type": "BACKEND_POST_TYPE_NORMAL", 4 | "creation_date": "2021-10-14T12:08:46.015Z", 5 | "text": { 6 | "no_flags": "Image test numero two:\nTwo images." 7 | }, 8 | "text_modules": [ 9 | { 10 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 11 | "display": "Image test numero two:", 12 | "target": null, 13 | "text_start": 0, 14 | "text_end": 22 15 | }, 16 | { 17 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 18 | "display": "\n", 19 | "target": null, 20 | "text_start": 22, 21 | "text_end": 23 22 | }, 23 | { 24 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 25 | "display": "Two images.", 26 | "target": null, 27 | "text_start": 23, 28 | "text_end": 34 29 | } 30 | ], 31 | "author": { 32 | "id": "106866867257179166", 33 | "display_name": "CawbirdTestAccount", 34 | "username": "cawbird_test_account", 35 | "avatar_url": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png" 36 | }, 37 | "attached_media": [ 38 | { 39 | "id": "107099801024363427", 40 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 41 | "alt_text": "The day variant of the Adwaita background.", 42 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/099/801/024/363/427/small/bda1a863501f4adb.png", 43 | "media_url": "https://files.mastodon.social/media_attachments/files/107/099/801/024/363/427/original/bda1a863501f4adb.png", 44 | "width": 1983, 45 | "height": 1046 46 | }, 47 | { 48 | "id": "107099801096765963", 49 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 50 | "alt_text": "The morning variant of the Adwaita background.", 51 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/099/801/096/765/963/small/a76d8ecfe32ef896.png", 52 | "media_url": "https://files.mastodon.social/media_attachments/files/107/099/801/096/765/963/original/a76d8ecfe32ef896.png", 53 | "width": 1983, 54 | "height": 1046 55 | } 56 | ], 57 | "domain": "mastodon.social", 58 | "url": "https://mastodon.social/@cawbird_test_account/107099804533083877", 59 | "source": "Web", 60 | "liked_count": 0, 61 | "replied_count": 0, 62 | "reposted_count": 0 63 | } 64 | -------------------------------------------------------------------------------- /tests/Parsing/MediaData/Mastodon/ThreePictureChecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "107099818747124648", 3 | "post_type": "BACKEND_POST_TYPE_NORMAL", 4 | "creation_date": "2021-10-14T12:12:22.895Z", 5 | "text": { 6 | "no_flags": "Now let's test three images!" 7 | }, 8 | "text_modules": [ 9 | { 10 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 11 | "display": "Now let's test three images!", 12 | "target": null, 13 | "text_start": 0, 14 | "text_end": 28 15 | } 16 | ], 17 | "author": { 18 | "id": "106866867257179166", 19 | "display_name": "CawbirdTestAccount", 20 | "username": "cawbird_test_account", 21 | "avatar_url": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png" 22 | }, 23 | "attached_media": [ 24 | { 25 | "id": "107099806776702939", 26 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 27 | "alt_text": null, 28 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/099/806/776/702/939/small/5ca9a4ba4dab7b5e.png", 29 | "media_url": "https://files.mastodon.social/media_attachments/files/107/099/806/776/702/939/original/5ca9a4ba4dab7b5e.png", 30 | "width": 1983, 31 | "height": 1046 32 | }, 33 | { 34 | "id": "107099806921406308", 35 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 36 | "alt_text": null, 37 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/099/806/921/406/308/small/82845727cafa8924.png", 38 | "media_url": "https://files.mastodon.social/media_attachments/files/107/099/806/921/406/308/original/82845727cafa8924.png", 39 | "width": 1983, 40 | "height": 1046 41 | }, 42 | { 43 | "id": "107099806964185381", 44 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 45 | "alt_text": null, 46 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/099/806/964/185/381/small/ed037671ef0afb1e.png", 47 | "media_url": "https://files.mastodon.social/media_attachments/files/107/099/806/964/185/381/original/ed037671ef0afb1e.png", 48 | "width": 1983, 49 | "height": 1046 50 | } 51 | ], 52 | "domain": "mastodon.social", 53 | "url": "https://mastodon.social/@cawbird_test_account/107099818747124648", 54 | "source": "Web", 55 | "liked_count": 0, 56 | "replied_count": 0, 57 | "reposted_count": 0 58 | } 59 | -------------------------------------------------------------------------------- /lib/Mastodon/Content/UserDataField.vala: -------------------------------------------------------------------------------- 1 | /* UserDataField.vala 2 | * 3 | * Copyright 2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * A field storing specific information about a User. 25 | */ 26 | public class Backend.Mastodon.UserDataField : Backend.UserDataField { 27 | 28 | /** 29 | * Parses the user data fields into a ListModel for a User. 30 | * 31 | * @param json The Json.Array containing the data fields. 32 | * 33 | * @return An ListModel with the data fields as UserDataField. 34 | */ 35 | internal static ListModel parse_list (Json.Array json) { 36 | var parsed_fields = new ListStore (typeof (UserDataField)); 37 | json.foreach_element ((array, index, element) => { 38 | if (element.get_node_type () == OBJECT) { 39 | Json.Object obj = element.get_object (); 40 | parsed_fields.append (new UserDataField (obj)); 41 | } 42 | }); 43 | return parsed_fields; 44 | } 45 | 46 | /** 47 | * Creates a new object from a provided JSON. 48 | * 49 | * @param json The json for this data field. 50 | */ 51 | private UserDataField (Json.Object json) { 52 | // Retrieve the content text surrounded by

so that TextParser can handle it. 53 | string field_text = "

" + json.get_string_member ("value") + "

"; 54 | TextModule[] field_mods = Utils.TextParser.instance.parse_text (field_text); 55 | 56 | // Construct object 57 | Object ( 58 | name: json.get_string_member ("name"), 59 | content: Backend.Utils.TextUtils.format_text (field_mods, false), 60 | verified: ! json.get_null_member ("verified_at") 61 | ? new DateTime.from_iso8601 ( 62 | json.get_string_member ("verified_at"), 63 | new TimeZone.utc ()) 64 | : null 65 | ); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Content/PostMetrics.vala: -------------------------------------------------------------------------------- 1 | /* PostMetrics.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Displays metrics about an post without any interactivity. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Content/PostMetrics.ui")] 27 | public class PostMetrics : Gtk.Widget { 28 | 29 | // UI-Elements of PostMetrics 30 | [GtkChild] 31 | private unowned Gtk.Box likes_box; 32 | [GtkChild] 33 | private unowned Gtk.Box reposts_box; 34 | [GtkChild] 35 | private unowned Gtk.Box replies_box; 36 | [GtkChild] 37 | private unowned Gtk.Label likes_counter; 38 | [GtkChild] 39 | private unowned Gtk.Label reposts_counter; 40 | [GtkChild] 41 | private unowned Gtk.Label replies_counter; 42 | 43 | /** 44 | * The post which metrics are displayed in this widget. 45 | */ 46 | public Backend.Post post { 47 | get { 48 | return displayed_post; 49 | } 50 | set { 51 | displayed_post = value; 52 | 53 | // Set the information on the UI 54 | likes_counter.label = displayed_post != null ? DisplayUtils.shortened_metric (displayed_post.liked_count) : "(null)"; 55 | reposts_counter.label = displayed_post != null ? DisplayUtils.shortened_metric (displayed_post.reposted_count) : "(null)"; 56 | replies_counter.label = displayed_post != null ? DisplayUtils.shortened_metric (displayed_post.replied_count) : "(null)"; 57 | } 58 | } 59 | 60 | /** 61 | * Deconstructs PostItem and it's childrens. 62 | */ 63 | public override void dispose () { 64 | // Destructs children of PostItem 65 | likes_box.unparent (); 66 | reposts_box.unparent (); 67 | replies_box.unparent (); 68 | base.dispose (); 69 | } 70 | 71 | /** 72 | * Stores the displayed repost. 73 | */ 74 | private Backend.Post? displayed_post = null; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /lib/Mastodon/Organization/HomeTimeline.vala: -------------------------------------------------------------------------------- 1 | /* HomeTimeline.vala 2 | * 3 | * Copyright 2022-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * The reverse chronological timeline with posts from all followed users. 25 | */ 26 | public class Backend.Mastodon.HomeTimeline : Backend.HomeTimeline { 27 | 28 | /** 29 | * Creates a HomeTimeline for a Session. 30 | * 31 | * In order to allow a ListView to include widgets before the posts, 32 | * the headers parameter can be added. For each string in that list 33 | * an PseudoItem will be created with the string as description. 34 | * 35 | * @param session The Session for which the timeline is created. 36 | * @param headers Descriptions for header items to be added. 37 | */ 38 | internal HomeTimeline (Session session, string[] headers = {}) { 39 | // Construct the object 40 | Object ( 41 | session: session, 42 | account: session.account, 43 | headers: headers 44 | ); 45 | } 46 | 47 | /** 48 | * Calls the API to retrieve all items from this Collection. 49 | * 50 | * @throws Error Any error while accessing the API and pulling the items. 51 | */ 52 | public override async void pull_items () throws Error { 53 | // Create the proxy call 54 | Rest.ProxyCall call = session.create_call (); 55 | call.set_method ("GET"); 56 | call.set_function (@"api/v1/timelines/home"); 57 | call.add_param ("limit", "50"); 58 | if (newest_item_id != null) { 59 | call.add_param ("min_id", newest_item_id); 60 | } 61 | 62 | // Load the timeline 63 | Json.Node json; 64 | try { 65 | json = yield session.server.call (call); 66 | } catch (Error e) { 67 | throw e; 68 | } 69 | 70 | // Load the posts in the post list 71 | add_items (session.load_post_list (json)); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/Preferences/SessionsPage.vala: -------------------------------------------------------------------------------- 1 | /* SessionsPage.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Lists all authenticated sessions and allows to change them. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Preferences/SessionsPage.ui")] 27 | public class Preferences.SessionsPage : Adw.PreferencesPage { 28 | 29 | // UI-Elements of SessionsPage 30 | [GtkChild] 31 | private unowned Gtk.ListBox session_list; 32 | 33 | /** 34 | * Run at construction of the page. 35 | */ 36 | construct { 37 | session_list.bind_model (Backend.Client.instance.sessions, bind_session); 38 | } 39 | 40 | /** 41 | * Activated when one of the listed sessions was activated. 42 | * 43 | * @param widget The widget that was clicked in the session list. 44 | */ 45 | [GtkCallback] 46 | private void display_session_settings (Gtk.ListBoxRow widget) { 47 | // Get the SessionRow 48 | var session_row = widget as SessionRow; 49 | if (session_row == null) { 50 | warning ("Activated row is not SessionRow!"); 51 | return; 52 | } 53 | 54 | // Get the PreferencesWindow 55 | var pref_window = this.get_root () as PreferencesWindow; 56 | if (pref_window == null) { 57 | warning ("SessionsPage not in a PreferencesWindow, action not possible!"); 58 | return; 59 | } 60 | 61 | // Open the new subview 62 | pref_window.display_session_settings (session_row.session); 63 | } 64 | 65 | /** 66 | * Binds an session to an SessionRow in the session list. 67 | */ 68 | private Gtk.Widget bind_session (Object item) { 69 | var session = item as Backend.Session; 70 | var widget = new SessionRow (); 71 | widget.show_actions = false; 72 | widget.show_next = true; 73 | widget.session = session; 74 | return widget; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /data/icons/symbolic/apps/uk.co.ibboard.Cawbird-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/symbolic/apps/uk.co.ibboard.Cawbird.Devel-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Authentication/LoadPage.vala: -------------------------------------------------------------------------------- 1 | /* LoadPage.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * The page where the account is loaded and stored. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Authentication/LoadPage.ui")] 27 | public class Authentication.LoadPage : Gtk.Widget { 28 | 29 | // UI-Elements of LoadPage 30 | [GtkChild] 31 | private unowned Adw.StatusPage page_content; 32 | [GtkChild] 33 | private unowned Gtk.Spinner load_indicator; 34 | 35 | /** 36 | * The AuthView holding this page. 37 | */ 38 | public weak AuthView view { get; construct; } 39 | 40 | /** 41 | * Run at construction of the widget. 42 | */ 43 | construct { 44 | // Check if children of AuthView 45 | if (view == null) { 46 | critical ("Can only be children to AuthView!"); 47 | } 48 | 49 | // Connect load stop 50 | view.changing_page.connect (stop_load); 51 | } 52 | 53 | /** 54 | * Begins the load of the page. 55 | */ 56 | public void begin_loading () { 57 | // Set the load indicator 58 | load_indicator.spinning = true; 59 | 60 | // Begin the loading 61 | run_loading.begin (); 62 | } 63 | 64 | /** 65 | * Run the loading. 66 | */ 67 | private async void run_loading () { 68 | // Authenticate handles a lot of this now 69 | view.move_to_next (); 70 | } 71 | 72 | /** 73 | * Run when moving back. 74 | */ 75 | private void stop_load () { 76 | // Cancel possible actions 77 | cancel_load.cancel (); 78 | 79 | // Stop load indicator 80 | load_indicator.spinning = false; 81 | } 82 | 83 | /** 84 | * Deconstructs LoadPage and it's childrens. 85 | */ 86 | public override void dispose () { 87 | // Deconstruct childrens 88 | page_content.unparent (); 89 | base.dispose (); 90 | } 91 | 92 | /** 93 | * Cancels the loading. 94 | */ 95 | private Cancellable? cancel_load = null; 96 | 97 | } 98 | -------------------------------------------------------------------------------- /tests/Parsing/MediaChecks.vala: -------------------------------------------------------------------------------- 1 | /* MediaChecks.vala 2 | * 3 | * Copyright 2021-2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Checks for testing a parsed Media object. 25 | */ 26 | namespace MediaChecks { 27 | 28 | /** 29 | * Tests all media in a post. 30 | * 31 | * @param post The Post to be checked. 32 | * @param check A Json.Object containing fields to check against. 33 | */ 34 | void check_all_media (Backend.Post post, Json.Object checks) { 35 | // Get all media and media checks 36 | Json.Array media_checks = checks.get_array_member ("attached_media"); 37 | Backend.Media[] media_objs = post.get_media (); 38 | TestUtils.check_integer ("All Media Count", media_objs.length, (int) media_checks.get_length ()); 39 | 40 | // Check each individual media 41 | media_checks.foreach_element ((array, index, element) => { 42 | Json.Object med_check = element.get_object (); 43 | Backend.Media media = media_objs [index]; 44 | 45 | // Run basic data checks 46 | check_basic_fields (media, med_check); 47 | }); 48 | } 49 | 50 | /** 51 | * Tests basic fields 52 | * 53 | * @param media The Media to be checked. 54 | * @param check A Json.Object containing fields to check against. 55 | */ 56 | void check_basic_fields (Backend.Media media, Json.Object check) { 57 | // Check id, type and alt_text 58 | TestUtils.check_string ("Media ID", media.id, check.get_string_member ("id")); 59 | TestUtils.check_string ("Media Type", media.media_type.to_string (), check.get_string_member ("type")); 60 | TestUtils.check_string ("Media Alt Text", media.alt_text, check.get_string_member ("alt_text")); 61 | 62 | // Check urls 63 | TestUtils.check_string ("Media Preview URL", media.preview_url, check.get_string_member ("preview_url")); 64 | TestUtils.check_string ("Media URL", media.media_url, check.get_string_member ("media_url")); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /lib/Base/Collections/CollectionCalls.vala: -------------------------------------------------------------------------------- 1 | /* CollectionCalls.vala 2 | * 3 | * Copyright 2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * An interface for Collection providing access methods for pullable lists. 25 | */ 26 | public interface Backend.PullableCollection : Backend.Collection { 27 | 28 | /** 29 | * The session used to pull posts. 30 | */ 31 | public abstract Session session { get; construct; } 32 | 33 | /** 34 | * The id of the newest item in the collection. 35 | */ 36 | protected abstract string? newest_item_id { get; set; default = null; } 37 | 38 | /** 39 | * Calls the API to retrieve all items from this Collection. 40 | * 41 | * @throws Error Any error while accessing the API and pulling the items. 42 | */ 43 | public abstract async void pull_items () throws Error; 44 | 45 | } 46 | 47 | /** 48 | * An interface for Collection providing access methods for paginated lists. 49 | */ 50 | public interface Backend.PaginatedCollection : Backend.Collection { 51 | 52 | /** 53 | * The session used to pull posts. 54 | */ 55 | public abstract Session session { get; construct; } 56 | 57 | /** 58 | * The id of the newest item in the collection. 59 | */ 60 | protected abstract string? newest_item_id { get; set; default = null; } 61 | 62 | /** 63 | * The id of the oldest item in the collection. 64 | */ 65 | protected abstract string? oldest_item_id { get; set; default = null; } 66 | 67 | /** 68 | * Calls the API to retrieve all new items from this Collection. 69 | * 70 | * @throws Error Any error while accessing the API and pulling the items. 71 | */ 72 | public abstract async void pull_new_items () throws Error; 73 | 74 | /** 75 | * Calls the API to retrieve older items from this Collection. 76 | * 77 | * @throws Error Any error while accessing the API and pulling the items. 78 | */ 79 | public abstract async void pull_old_items () throws Error; 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/Collections/CollectionFilter.vala: -------------------------------------------------------------------------------- 1 | /* CollectionFilter.vala 2 | * 3 | * Copyright 2022-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Provides the user the FilterButtons to filter posts. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Collections/CollectionFilter.ui")] 27 | public class CollectionFilter : Gtk.Widget { 28 | 29 | // UI-Elements of CollectionFilter 30 | [GtkChild] 31 | private unowned Gtk.FlowBox filter_box; 32 | [GtkChild] 33 | private unowned FilterButton generic_filter; 34 | [GtkChild] 35 | private unowned FilterButton reposts_filter; 36 | 37 | /** 38 | * Which platform the displayed collection is on. 39 | * 40 | * Used to determine a few platform-specific strings. 41 | */ 42 | public Backend.PlatformEnum displayed_platform { 43 | get { 44 | return set_display_platform; 45 | } 46 | set { 47 | set_display_platform = value; 48 | switch (set_display_platform) { 49 | default: 50 | generic_filter.label = _("Posts"); 51 | reposts_filter.label = _("Reposts"); 52 | break; 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * If generic posts should be displayed. 59 | */ 60 | public bool display_generic { get; set; } 61 | 62 | /** 63 | * If replies should be displayed. 64 | */ 65 | public bool display_replies { get; set; } 66 | 67 | /** 68 | * If reposts should be displayed. 69 | */ 70 | public bool display_reposts { get; set; } 71 | 72 | /** 73 | * If media posts should be displayed. 74 | */ 75 | public bool display_media { get; set; } 76 | 77 | /** 78 | * Deconstructs CollectionFilter and it's childrens. 79 | */ 80 | public override void dispose () { 81 | // Destructs children of CollectionFilter 82 | filter_box.unparent (); 83 | base.dispose (); 84 | } 85 | 86 | /** 87 | * Store the display platform. 88 | */ 89 | private Backend.PlatformEnum set_display_platform; 90 | 91 | } 92 | -------------------------------------------------------------------------------- /lib/meson.build: -------------------------------------------------------------------------------- 1 | # Cawbird backend build file 2 | 3 | # Backend dependencies 4 | libdepends = [ 5 | dependency('glib-2.0', version: '>= 2.50'), 6 | dependency('gtk4', version: '>= 4.0'), 7 | dependency('json-glib-1.0', version: '>= 1.2'), 8 | dependency('libsoup-3.0', version: '>= 3.0'), 9 | dependency('rest-1.0', version: '>= 0.9'), 10 | dependency('libsecret-1', version: '>= 0.20.5') 11 | ] 12 | 13 | # Add libxml as dependency for Mastodon 14 | if mastodon_backend 15 | libdepends += dependency('libxml-2.0', version: '>= 2.9.12') 16 | endif 17 | 18 | # Base files 19 | libfiles = [ 20 | 'Base/Collections/Collection.vala', 21 | 'Base/Collections/CollectionCalls.vala', 22 | 'Base/Collections/CollectionFilters.vala', 23 | 'Base/Collections/CollectionHeaders.vala', 24 | 'Base/Collections/CollectionPins.vala', 25 | 'Base/Collections/FilteredCollection.vala', 26 | 'Base/Collections/PostConnections.vala', 27 | 'Base/Content/Media.vala', 28 | 'Base/Content/MediaEnums.vala', 29 | 'Base/Content/Post.vala', 30 | 'Base/Content/PostAuxiliary.vala', 31 | 'Base/Content/TextModule.vala', 32 | 'Base/Content/User.vala', 33 | 'Base/Content/UserDataField.vala', 34 | 'Base/Content/UserEnums.vala', 35 | 'Base/Organization/HomeTimeline.vala', 36 | 'Base/Organization/Thread.vala', 37 | 'Base/Organization/UserTimeline.vala', 38 | 'Base/System/Client.vala', 39 | 'Base/System/ClientLists.vala', 40 | 'Base/System/ClientState.vala', 41 | 'Base/System/Server.vala', 42 | 'Base/System/Session.vala', 43 | 'Base/System/SessionAuth.vala', 44 | 'Base/Utils/Examples.vala', 45 | 'Base/Utils/KeyStorage.vala', 46 | 'Base/Utils/MediaLoader.vala', 47 | 'Base/Utils/PlatformEnum.vala', 48 | 'Base/Utils/StateIO.vala', 49 | 'Base/Utils/TextFormats.vala', 50 | 'Base/Utils/TextUtils.vala' 51 | ] 52 | 53 | # Add files for active backends 54 | if mastodon_backend 55 | libfiles += [ 56 | 'Mastodon/Content/Media.vala', 57 | 'Mastodon/Content/Post.vala', 58 | 'Mastodon/Content/User.vala', 59 | 'Mastodon/Content/UserDataField.vala', 60 | 'Mastodon/Organization/HomeTimeline.vala', 61 | 'Mastodon/Organization/Thread.vala', 62 | 'Mastodon/Organization/UserTimeline.vala', 63 | 'Mastodon/System/Server.vala', 64 | 'Mastodon/System/Session.vala', 65 | 'Mastodon/System/SessionAuth.vala', 66 | 'Mastodon/System/SessionCalls.vala', 67 | 'Mastodon/Utils/ParseUtils.vala', 68 | 'Mastodon/Utils/TextParser.vala' 69 | ] 70 | endif 71 | 72 | backendsrc = include_directories('.') 73 | 74 | # Build the library 75 | backendlib = library( 76 | 'cawbird', 77 | libfiles, 78 | dependencies: libdepends, 79 | vala_vapi: 'libcawbird.vapi', 80 | install: true 81 | ) 82 | -------------------------------------------------------------------------------- /lib/Base/System/SessionAuth.vala: -------------------------------------------------------------------------------- 1 | /* SessionAuth.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Provides utilities to authenticate new sessions. 25 | */ 26 | public abstract class Backend.SessionAuth : Object { 27 | 28 | /** 29 | * Initializes the authentication. 30 | * 31 | * Initializes the authentication proxy at the set server. 32 | * If the client was not authenticated at the given server, 33 | * it will run the authentication for it. 34 | * 35 | * On platforms with only one server (e.g. Twitter), the domain 36 | * parameter is ignored. 37 | * 38 | * @param domain The domain of the server to authenticate at. 39 | * 40 | * @throws Error Errors that happen when the server could not be set. 41 | */ 42 | public abstract async void init_auth (string domain) throws Error; 43 | 44 | /** 45 | * Generates an authentication url to begin an authentication. 46 | * 47 | * The returned url should be opened in the default browser, 48 | * so the user can authenticate the client at the platforms server. 49 | * 50 | * @param use_redirect If the clients redirect should be used. 51 | * 52 | * @return The link with the site to authenticate the user. 53 | */ 54 | public abstract string auth_request (bool use_redirect = true); 55 | 56 | /** 57 | * Finishes the authentication and creates the Session. 58 | * 59 | * This finishes the authentication by retrieving the access token 60 | * using the authentication code the user retrieved from the 61 | * authentication server, and creates a new Session. 62 | * 63 | * @param auth_code The authentication code for the user. 64 | * @param state An additional code verified locally. 65 | * 66 | * @return The newly authenticated session. 67 | * 68 | * @throws Error Any error occurring while finishing the authentication. 69 | */ 70 | public abstract async Session authenticate (string auth_code, string? state = null) throws Error; 71 | 72 | } 73 | -------------------------------------------------------------------------------- /lib/Base/Collections/CollectionHeaders.vala: -------------------------------------------------------------------------------- 1 | /* CollectionHeaders.vala 2 | * 3 | * Copyright 2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * An interface for Collection providing custom headers. 25 | * 26 | * A header item is added by a array of strings. Each string 27 | * generates a HeaderItem object, which will be placed in the collection. 28 | * These items can be used by an client to add special widgets to a ListView. 29 | */ 30 | public interface Backend.CollectionHeaders : Backend.Collection { 31 | 32 | /** 33 | * The strings used to generated the items. 34 | */ 35 | public abstract string[] headers { get; construct; } 36 | 37 | /** 38 | * Generates the headers from the property. 39 | * 40 | * Should be run on construction of implementing classes. 41 | * 42 | * @return The header items to be added to the collection. 43 | */ 44 | protected HeaderItem[] generate_headers () { 45 | HeaderItem[] items = {}; 46 | uint header_i = 0; 47 | foreach (string title in headers) { 48 | items += new HeaderItem (header_i, title); 49 | header_i++; 50 | } 51 | return items; 52 | } 53 | 54 | } 55 | 56 | /** 57 | * A header item generated by CollectionHeaders. 58 | */ 59 | public class Backend.HeaderItem : Object { 60 | 61 | /** 62 | * An index used while sorting the collection to order multiple HeaderItems. 63 | */ 64 | public uint index { get; construct; } 65 | 66 | /** 67 | * An description for the UI to place the right widget. 68 | */ 69 | public string description { get; construct; } 70 | 71 | /** 72 | * Creates an instance of this object. 73 | * 74 | * @param index An index used while sorting the collection to order multiple HeaderItems. 75 | * @param description An description for the UI to place the right widget. 76 | */ 77 | internal HeaderItem (uint index, string description) { 78 | Object ( 79 | index: index, 80 | description: description 81 | ); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /tests/Parsing/PostData/Mastodon/BasicPost.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "106883926076492274", 3 | "created_at": "2021-09-06T09:08:02.188Z", 4 | "in_reply_to_id": null, 5 | "in_reply_to_account_id": null, 6 | "sensitive": false, 7 | "spoiler_text": "", 8 | "visibility": "public", 9 | "language": "en", 10 | "uri": "https://mastodon.social/users/cawbird_test_account/statuses/106883926076492274", 11 | "url": "https://mastodon.social/@cawbird_test_account/106883926076492274", 12 | "replies_count": 0, 13 | "reblogs_count": 1, 14 | "favourites_count": 0, 15 | "edited_at": null, 16 | "content": "

Hello World!

This is a basic post with some text in it. Nothing more...

I mean, this is a test profil after all, so don't expect an interesting text.

", 17 | "reblog": null, 18 | "application": { 19 | "name": "Web", 20 | "website": null 21 | }, 22 | "account": { 23 | "id": "106866867257179166", 24 | "username": "cawbird_test_account", 25 | "acct": "cawbird_test_account", 26 | "display_name": "CawbirdTestAccount", 27 | "locked": false, 28 | "bot": false, 29 | "discoverable": false, 30 | "group": false, 31 | "created_at": "2021-09-03T00:00:00.000Z", 32 | "note": "

This may or may not be a account from @CodedOre for testing some API's for a certain Client.

", 33 | "url": "https://mastodon.social/@cawbird_test_account", 34 | "avatar": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png", 35 | "avatar_static": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png", 36 | "header": "https://files.mastodon.social/accounts/headers/106/866/867/257/179/166/original/1b43374005561eec.jpeg", 37 | "header_static": "https://files.mastodon.social/accounts/headers/106/866/867/257/179/166/original/1b43374005561eec.jpeg", 38 | "followers_count": 3, 39 | "following_count": 0, 40 | "statuses_count": 11, 41 | "last_status_at": "2021-12-20", 42 | "emojis": [], 43 | "fields": [ 44 | { 45 | "name": "Lives in", 46 | "value": "org.gnome.Sdk", 47 | "verified_at": null 48 | }, 49 | { 50 | "name": "Example Weblink", 51 | "value": "https://example.com", 52 | "verified_at": null 53 | } 54 | ] 55 | }, 56 | "media_attachments": [], 57 | "mentions": [], 58 | "tags": [], 59 | "emojis": [], 60 | "card": null, 61 | "poll": null 62 | } 63 | -------------------------------------------------------------------------------- /src/Pages/UserPage.vala: -------------------------------------------------------------------------------- 1 | /* UserPage.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Displays an User and Posts related to him. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Pages/UserPage.ui")] 27 | public class UserPage : Gtk.Widget { 28 | 29 | // General UI-Elements of UserPage 30 | [GtkChild] 31 | private unowned Adw.HeaderBar page_header; 32 | [GtkChild] 33 | private unowned Adw.WindowTitle page_title; 34 | [GtkChild] 35 | private unowned CollectionView collection_view; 36 | 37 | /** 38 | * The User which is displayed. 39 | */ 40 | public Backend.User user { 41 | get { 42 | return displayed_user; 43 | } 44 | set { 45 | displayed_user = value; 46 | 47 | // Get the session for the widget 48 | var main_window = this.get_root () as MainWindow; 49 | var session = main_window != null 50 | ? main_window.session 51 | : null; 52 | 53 | // Retrieve the UserTimeline 54 | timeline = session != null && displayed_user != null 55 | ? session.get_user_timeline (displayed_user, CollectionView.HEADERS) 56 | : null; 57 | 58 | // Set the page content 59 | page_title.subtitle = session != null ? session.account.username : null; 60 | collection_view.collection = timeline; 61 | } 62 | } 63 | 64 | /** 65 | * Run at construction of the widget. 66 | */ 67 | construct { 68 | page_title.title = Config.PROJECT_NAME; 69 | } 70 | 71 | /** 72 | * Deconstructs UserPage and it's childrens. 73 | */ 74 | public override void dispose () { 75 | // Destructs children of UserPage 76 | page_header.unparent (); 77 | collection_view.unparent (); 78 | base.dispose (); 79 | } 80 | 81 | /** 82 | * Stores the UserTimeline displayed for this user. 83 | */ 84 | private Backend.UserTimeline? timeline = null; 85 | 86 | /** 87 | * Stores the displayed user. 88 | */ 89 | private Backend.User? displayed_user = null; 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | # Cawbird client build file 2 | 3 | # Dependencies for the client 4 | cawdepends = [ 5 | dependency('gio-2.0', version: '>= 2.50'), 6 | dependency('glib-2.0', version: '>= 2.7.1'), 7 | dependency('gtk4', version: '>= 4.7'), 8 | dependency('libadwaita-1', version: '>= 1.2'), 9 | # Compile with GLib Math support. 10 | meson.get_compiler('c').find_library('m', required: false) 11 | ] 12 | 13 | # Link backend to client 14 | cawdepends += declare_dependency( 15 | link_with: backendlib, 16 | include_directories: backendsrc, 17 | dependencies: libdepends 18 | ) 19 | 20 | # Sources of the client 21 | cawsources = cawresources 22 | cawsources += [ 23 | 'config.vapi', 24 | 'Cawbird.vala', 25 | 'MainWindow.vala', 26 | 'Authentication/AuthView.vala', 27 | 'Authentication/BrowserPage.vala', 28 | 'Authentication/CodePage.vala', 29 | 'Authentication/FinalPage.vala', 30 | 'Authentication/LoadPage.vala', 31 | 'Authentication/ServerPage.vala', 32 | 'Collections/CollectionFilter.vala', 33 | 'Collections/CollectionView.vala', 34 | 'Content/PostActions.vala', 35 | 'Content/PostContent.vala', 36 | 'Content/PostItem.vala', 37 | 'Content/PostMetrics.vala', 38 | 'Content/PostStatus.vala', 39 | 'Content/UserCard.vala', 40 | 'Content/UserDataDisplay.vala', 41 | 'Content/UserDisplay.vala', 42 | 'Media/MediaDialog.vala', 43 | 'Media/MediaDisplay.vala', 44 | 'Media/MediaDisplayItem.vala', 45 | 'Media/MediaPreview.vala', 46 | 'Media/MediaSelector.vala', 47 | 'Pages/MainPage.vala', 48 | 'Pages/ThreadPage.vala', 49 | 'Pages/UserPage.vala', 50 | 'Preferences/AppearancesPage.vala', 51 | 'Preferences/PreferencesWindow.vala', 52 | 'Preferences/SessionSettings.vala', 53 | 'Preferences/SessionsPage.vala', 54 | 'Preferences/WindowManagement.vala', 55 | 'Utils/DisplayUtils.vala', 56 | 'Utils/SystemInfo.vala', 57 | 'Widgets/BadgesBox.vala', 58 | 'Widgets/FilterButton.vala', 59 | 'Widgets/CroppedPicture.vala', 60 | 'Widgets/SessionRow.vala', 61 | 'Widgets/SessionSidebar.vala', 62 | 'Widgets/UserAvatar.vala', 63 | 'Widgets/UserButton.vala', 64 | 'Widgets/WaitingButton.vala' 65 | ] 66 | 67 | # Add build configurations 68 | cdata = configuration_data() 69 | cdata.set_quoted ('PROJECT_NAME', meson.project_name()) 70 | cdata.set_quoted ('PROJECT_VERSION', version) 71 | cdata.set_quoted ('APPLICATION_ID', application_id) 72 | cdata.set_quoted ('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) 73 | configure_file( 74 | output: 'config.h', 75 | configuration: cdata 76 | ) 77 | 78 | # Build the client 79 | executable( 80 | 'cawbird', 81 | cawsources, 82 | dependencies: cawdepends, 83 | vala_args: [ 84 | '--target-glib=2.50', 85 | '--gresourcesdir=ui/' 86 | ], 87 | install: true 88 | ) 89 | -------------------------------------------------------------------------------- /data/icons/scalable/actions/reposted-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 36 | 39 | 40 | 43 | 47 | 53 | 58 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/Pages/ThreadPage.vala: -------------------------------------------------------------------------------- 1 | /* ThreadPage.vala 2 | * 3 | * Copyright 2022-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Displays an Thread on it's page. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Pages/ThreadPage.ui")] 27 | public class ThreadPage : Gtk.Widget { 28 | 29 | // General UI-Elements of ThreadPage 30 | [GtkChild] 31 | private unowned Adw.HeaderBar page_header; 32 | [GtkChild] 33 | private unowned Adw.WindowTitle page_title; 34 | [GtkChild] 35 | private unowned CollectionView collection_view; 36 | 37 | /** 38 | * The Post which it the main one in this post. 39 | */ 40 | public Backend.Post post { 41 | get { 42 | return displayed_post; 43 | } 44 | set { 45 | displayed_post = value; 46 | 47 | // Get the session for the widget 48 | var main_window = this.get_root () as MainWindow; 49 | var session = main_window != null 50 | ? main_window.session 51 | : null; 52 | 53 | // Retrieve the Thread 54 | thread = session != null && displayed_post != null 55 | ? session.get_thread (displayed_post) 56 | : null; 57 | 58 | // Set the page content 59 | page_title.subtitle = session != null ? session.account.username : null; 60 | collection_view.main_post_id = displayed_post != null ? displayed_post.id : null; 61 | collection_view.collection = thread; 62 | } 63 | } 64 | 65 | /** 66 | * Run at construction of the widget. 67 | */ 68 | construct { 69 | page_title.title = Config.PROJECT_NAME; 70 | } 71 | 72 | /** 73 | * Deconstructs ThreadPage and it's childrens. 74 | */ 75 | public override void dispose () { 76 | // Destructs children of ThreadPage 77 | page_header.unparent (); 78 | collection_view.unparent (); 79 | base.dispose (); 80 | } 81 | 82 | /** 83 | * Stores the displayed post. 84 | */ 85 | private Backend.Post? displayed_post = null; 86 | 87 | /** 88 | * Stores the Thread displayed in this page. 89 | */ 90 | private Backend.Thread? thread = null; 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Widgets/SessionRow.vala: -------------------------------------------------------------------------------- 1 | /* SessionRow.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Shows an account in an row and offers options for it. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Widgets/SessionRow.ui")] 27 | public class SessionRow : Adw.ActionRow { 28 | 29 | // UI-Elements of SessionRow 30 | [GtkChild] 31 | private unowned UserAvatar account_avatar; 32 | 33 | /** 34 | * If additional actions should be shown. 35 | */ 36 | public bool show_actions { get; set; default = true; } 37 | 38 | /** 39 | * If additional actions should be shown. 40 | */ 41 | public bool show_next { get; set; default = false; } 42 | 43 | /** 44 | * The Session which is displayed. 45 | */ 46 | public Backend.Session session { 47 | get { 48 | return displayed_session; 49 | } 50 | set { 51 | displayed_session = value; 52 | 53 | // Set the information in the UI 54 | account_avatar.user = displayed_session != null ? displayed_session.account : null; 55 | this.title = displayed_session != null ? displayed_session.account.display_name : "(null)"; 56 | this.subtitle = displayed_session != null ? DisplayUtils.prefix_username (displayed_session.account) : "(null)"; 57 | } 58 | } 59 | 60 | /** 61 | * Opens the displayed session in a new Window. 62 | */ 63 | [GtkCallback] 64 | private void open_in_window () { 65 | // Only continue with an set session 66 | if (session == null) { 67 | return; 68 | } 69 | 70 | // Get the current MainWindow, to get the application 71 | var main_window = this.get_root () as MainWindow; 72 | if (main_window == null) { 73 | warning ("SessionRow not in a MainWindow, action not possible!"); 74 | return; 75 | } 76 | 77 | // Create a new MainWindow and display the session 78 | var window = new MainWindow (main_window.application as Cawbird, session); 79 | window.present (); 80 | } 81 | 82 | /** 83 | * Stores the displayed session. 84 | */ 85 | private Backend.Session? displayed_session = null; 86 | 87 | } 88 | -------------------------------------------------------------------------------- /ui/Content/UserDisplay.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template UserDisplay : Gtk.Widget { 5 | overflow: hidden; 6 | valign: start; 7 | layout-manager: Gtk.BoxLayout { 8 | orientation: vertical; 9 | }; 10 | 11 | .UserCard user_card { 12 | user: bind UserDisplay.user; 13 | } 14 | 15 | Gtk.Box user_infobox { 16 | margin-start: 20; 17 | margin-end: 20; 18 | margin-bottom: 16; 19 | orientation: vertical; 20 | 21 | Gtk.Box { 22 | spacing: 4; 23 | 24 | Gtk.Label user_display_label { 25 | xalign: 0.0; 26 | ellipsize: end; 27 | 28 | styles [ 29 | "title-1" 30 | ] 31 | } 32 | 33 | .BadgesBox user_badges { 34 | icon-size: 12; 35 | valign: start; 36 | } 37 | } 38 | 39 | Gtk.Label user_username_label { 40 | xalign: 0.0; 41 | ellipsize: end; 42 | margin-bottom: 8; 43 | 44 | styles [ 45 | "caption", 46 | "dim-label" 47 | ] 48 | } 49 | 50 | Gtk.Label user_description_label { 51 | xalign: 0.0; 52 | wrap: true; 53 | wrap-mode: word_char; 54 | use-markup: true; 55 | margin-bottom: 8; 56 | 57 | activate-link => on_link_clicked (); 58 | } 59 | 60 | Gtk.FlowBox { 61 | selection-mode: none; 62 | column-spacing: 8; 63 | max-children-per-line: 42; 64 | margin-bottom: 8; 65 | 66 | Gtk.Box { 67 | spacing: 4; 68 | 69 | Gtk.Image creation_icon { 70 | icon-name: "x-office-calendar-symbolic"; 71 | } 72 | 73 | Gtk.Label creation_label {} 74 | } 75 | 76 | Gtk.Button { 77 | styles [ 78 | "flat", 79 | "inline" 80 | ] 81 | 82 | Gtk.Box { 83 | spacing: 2; 84 | 85 | Gtk.Label following_counter { 86 | use-markup: true; 87 | 88 | styles [ 89 | "body" 90 | ] 91 | } 92 | 93 | Gtk.Image { 94 | icon-name: "go-next-symbolic"; 95 | } 96 | } 97 | } 98 | 99 | Gtk.Button { 100 | styles [ 101 | "flat", 102 | "inline" 103 | ] 104 | 105 | Gtk.Box { 106 | spacing: 2; 107 | 108 | Gtk.Label followers_counter { 109 | use-markup: true; 110 | 111 | styles [ 112 | "body" 113 | ] 114 | } 115 | 116 | Gtk.Image { 117 | icon-name: "go-next-symbolic"; 118 | } 119 | } 120 | } 121 | } 122 | 123 | Gtk.FlowBox user_fields { 124 | selection-mode: none; 125 | column-spacing: 8; 126 | max-children-per-line: 42; 127 | margin-bottom: 8; 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /lib/Mastodon/Organization/Thread.vala: -------------------------------------------------------------------------------- 1 | /* Thread.vala 2 | * 3 | * Copyright 2022-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Provides the utilities to display a thread based on a post. 25 | */ 26 | public class Backend.Mastodon.Thread : Backend.Thread { 27 | 28 | /** 29 | * Creates a new Thread object for a given main post. 30 | * 31 | * @param session The Session that this thread is assigned to. 32 | * @param main_post The main post which serves as the focus for this thread. 33 | */ 34 | internal Thread (Session session, Backend.Post main_post) { 35 | // Construct the object 36 | Object ( 37 | session: session, 38 | main_post: main_post 39 | ); 40 | 41 | // Append the main post to the list 42 | add_item (main_post); 43 | } 44 | 45 | /** 46 | * Calls the API to get the posts for the Collection. 47 | * 48 | * @throws Error Any error that happened while pulling the posts. 49 | */ 50 | public override async void pull_items () throws Error { 51 | // If main post is a repost, we use the referenced post 52 | var pull_post = main_post.post_type == REPOST 53 | ? main_post.referenced_post 54 | : main_post; 55 | 56 | // Create the proxy call 57 | Rest.ProxyCall call = session.create_call (); 58 | call.set_method ("GET"); 59 | call.set_function (@"api/v1/statuses/$(pull_post.id)/context"); 60 | 61 | // Load the timeline 62 | Json.Node json; 63 | try { 64 | json = yield session.server.call (call); 65 | } catch (Error e) { 66 | throw e; 67 | } 68 | 69 | // Split the returned json in preceding and following 70 | Json.Object data = json.get_object (); 71 | var preceding = new Json.Node.alloc (); 72 | var following = new Json.Node.alloc (); 73 | preceding.init_array (data.get_array_member ("ancestors")); 74 | following.init_array (data.get_array_member ("descendants")); 75 | 76 | // Load the posts in the post list 77 | foreach (Backend.Post post in session.load_post_list (preceding)) { 78 | add_item (post); 79 | } 80 | foreach (Backend.Post post in session.load_post_list (following)) { 81 | add_item (post); 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /tests/Parsing/PostData/Mastodon/RepostChecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "107020119650688309", 3 | "post_type": "BACKEND_POST_TYPE_REPOST", 4 | "creation_date": "2021-09-30T10:23:51.196Z", 5 | "text": { 6 | "no_flags": "" 7 | }, 8 | "text_modules": [], 9 | "author": { 10 | "id": "106866867257179166", 11 | "display_name": "CawbirdTestAccount", 12 | "username": "cawbird_test_account", 13 | "avatar_url": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png" 14 | }, 15 | "source": "Undefined", 16 | "referenced_post": { 17 | "id": "106883926076492274", 18 | "post_type": "BACKEND_POST_TYPE_NORMAL", 19 | "creation_date": "2021-09-06T09:08:02.188Z", 20 | "text": { 21 | "no_flags": "Hello World!\n\nThis is a basic post with some text in it. Nothing more...\n\nI mean, this is a test profil after all, so don't expect an interesting text." 22 | }, 23 | "text_modules": [ 24 | { 25 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 26 | "display": "Hello World!", 27 | "target": null, 28 | "text_start": 0, 29 | "text_end": 12 30 | }, 31 | { 32 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 33 | "display": "\n\n", 34 | "target": null, 35 | "text_start": 12, 36 | "text_end": 14 37 | }, 38 | { 39 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 40 | "display": "This is a basic post with some text in it. Nothing more...", 41 | "target": null, 42 | "text_start": 14, 43 | "text_end": 72 44 | }, 45 | { 46 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 47 | "display": "\n\n", 48 | "target": null, 49 | "text_start": 72, 50 | "text_end": 74 51 | }, 52 | { 53 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 54 | "display": "I mean, this is a test profil after all, so don't expect an interesting text.", 55 | "target": null, 56 | "text_start": 74, 57 | "text_end": 151 58 | } 59 | ], 60 | "author": { 61 | "id": "106866867257179166", 62 | "display_name": "CawbirdTestAccount", 63 | "username": "cawbird_test_account", 64 | "avatar_url": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png" 65 | }, 66 | "domain": "mastodon.social", 67 | "url": "https://mastodon.social/@cawbird_test_account/106883926076492274", 68 | "source": "Web", 69 | "liked_count": 0, 70 | "replied_count": 0, 71 | "reposted_count": 1 72 | }, 73 | "domain": "mastodon.social", 74 | "url": "https://mastodon.social/users/cawbird_test_account/statuses/107020119650688309/activity", 75 | "liked_count": 0, 76 | "replied_count": 0, 77 | "reposted_count": 0 78 | } 79 | -------------------------------------------------------------------------------- /src/Pages/MainPage.vala: -------------------------------------------------------------------------------- 1 | /* MainPage.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * The start page for the user, provides the main views for the program. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Pages/MainPage.ui")] 27 | public class MainPage : Gtk.Widget { 28 | 29 | // UI-Elements of MainPage 30 | [GtkChild] 31 | private unowned Adw.Flap page_flap; 32 | 33 | // UI-Elements of the content 34 | [GtkChild] 35 | private unowned Adw.WindowTitle content_title; 36 | [GtkChild] 37 | private unowned CollectionView home_collection; 38 | 39 | // UI-Elements of the flap 40 | [GtkChild] 41 | private unowned Adw.WindowTitle flap_title; 42 | [GtkChild] 43 | private unowned SessionSidebar session_sidebar; 44 | 45 | /** 46 | * Run at construction of the widget. 47 | */ 48 | construct { 49 | content_title.title = Config.PROJECT_NAME; 50 | flap_title.title = Config.PROJECT_NAME; 51 | } 52 | 53 | /** 54 | * The Session which is displayed. 55 | */ 56 | public Backend.Session session { 57 | get { 58 | return displayed_session; 59 | } 60 | set { 61 | displayed_session = value; 62 | 63 | // Retrieve the UserTimeline 64 | timeline = displayed_session != null 65 | ? session.get_home_timeline (CollectionView.HEADERS) 66 | : null; 67 | 68 | // Set the page content 69 | home_collection.collection = timeline; 70 | content_title.subtitle = displayed_session != null 71 | ? displayed_session.account.username 72 | : null; 73 | 74 | // Set the active account in the sidebar 75 | session_sidebar.active_session = displayed_session; 76 | } 77 | } 78 | 79 | /** 80 | * Deconstructs MainPage and it's childrens. 81 | */ 82 | public override void dispose () { 83 | // Destructs children of MainPage 84 | page_flap.unparent (); 85 | base.dispose (); 86 | } 87 | 88 | /** 89 | * Stores the HomeTimeline displayed 90 | */ 91 | private Backend.HomeTimeline? timeline = null; 92 | 93 | /** 94 | * Stores the displayed session. 95 | */ 96 | private Backend.Session? displayed_session = null; 97 | 98 | } 99 | -------------------------------------------------------------------------------- /lib/Base/Utils/StateIO.vala: -------------------------------------------------------------------------------- 1 | /* WindowManagement.vala 2 | * 3 | * Copyright 2022-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | namespace Backend.Utils.StateIO { 24 | /** 25 | * Loads a GVariant from the state file. 26 | * 27 | * @param state_path The path to store the state in 28 | * @param file_name The file name to store 29 | * 30 | * @return The GVariant from the file, or null if not existing. 31 | * 32 | * @throws Error Errors while accessing the state file. 33 | */ 34 | public Variant? load_file (string state_path, string file_name) throws Error { 35 | // Initialize the file 36 | var file = File.new_build_filename (state_path, file_name, null); 37 | 38 | Variant? stored_state; 39 | try { 40 | // Load the data from the file 41 | uint8[] file_content; 42 | string file_etag; 43 | file.load_contents (null, out file_content, out file_etag); 44 | // Convert the file data to an Variant and read the values from it 45 | var stored_bytes = new Bytes.take (file_content); 46 | stored_state = new Variant.from_bytes (new VariantType ("a{sv}"), stored_bytes, false); 47 | } catch (Error e) { 48 | // Don't put warning out if the file can't be found (expected error) 49 | if (! (e is IOError.NOT_FOUND)) { 50 | throw e; 51 | } 52 | stored_state = null; 53 | } 54 | return stored_state; 55 | } 56 | 57 | /** 58 | * Stores a GVariant to the state file. 59 | * 60 | * 61 | * @param state_path The path to store the state in 62 | * @param file_name The file name to store 63 | * @param variant The GVariant to be stored. 64 | * 65 | * @throws Error Errors while accessing the state file. 66 | */ 67 | public void store_file (string state_path, string file_name, Variant variant) throws Error { 68 | // Initialize the file 69 | var file = File.new_build_filename (state_path, file_name, null); 70 | 71 | // Convert the variant to Bytes and store to file 72 | try { 73 | Bytes bytes = variant.get_data_as_bytes (); 74 | file.replace_contents (bytes.get_data (), null, 75 | false, REPLACE_DESTINATION, 76 | null, null); 77 | } catch (Error e) { 78 | throw e; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Parsing/MediaData/Mastodon/FourPictureChecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "107099829696203153", 3 | "post_type": "BACKEND_POST_TYPE_NORMAL", 4 | "creation_date": "2021-10-14T12:15:09.966Z", 5 | "text": { 6 | "no_flags": "And at last, now to show the maximum number I will test: 4!" 7 | }, 8 | "text_modules": [ 9 | { 10 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 11 | "display": "And at last, now to show the maximum number I will test: 4!", 12 | "target": null, 13 | "text_start": 0, 14 | "text_end": 59 15 | } 16 | ], 17 | "author": { 18 | "id": "106866867257179166", 19 | "display_name": "CawbirdTestAccount", 20 | "username": "cawbird_test_account", 21 | "avatar_url": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png" 22 | }, 23 | "attached_media": [ 24 | { 25 | "id": "107099825678531258", 26 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 27 | "alt_text": null, 28 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/099/825/678/531/258/small/a9097b06bcef7114.png", 29 | "media_url": "https://files.mastodon.social/media_attachments/files/107/099/825/678/531/258/original/a9097b06bcef7114.png", 30 | "width": 1983, 31 | "height": 1046 32 | }, 33 | { 34 | "id": "107099826661052081", 35 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 36 | "alt_text": null, 37 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/099/826/661/052/081/small/cbfbf77f3d170aff.png", 38 | "media_url": "https://files.mastodon.social/media_attachments/files/107/099/826/661/052/081/original/cbfbf77f3d170aff.png", 39 | "width": 1983, 40 | "height": 1046 41 | }, 42 | { 43 | "id": "107099826664431203", 44 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 45 | "alt_text": null, 46 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/099/826/664/431/203/small/6c711c63225f14ea.png", 47 | "media_url": "https://files.mastodon.social/media_attachments/files/107/099/826/664/431/203/original/6c711c63225f14ea.png", 48 | "width": 1983, 49 | "height": 1046 50 | }, 51 | { 52 | "id": "107099826684465663", 53 | "type": "BACKEND_MEDIA_TYPE_PICTURE", 54 | "alt_text": null, 55 | "preview_url": "https://files.mastodon.social/media_attachments/files/107/099/826/684/465/663/small/21a1d8a442677fc4.png", 56 | "media_url": "https://files.mastodon.social/media_attachments/files/107/099/826/684/465/663/original/21a1d8a442677fc4.png", 57 | "width": 1983, 58 | "height": 1046 59 | } 60 | ], 61 | "domain": "mastodon.social", 62 | "url": "https://mastodon.social/@cawbird_test_account/107099829696203153", 63 | "source": "Web", 64 | "liked_count": 0, 65 | "replied_count": 0, 66 | "reposted_count": 0 67 | } 68 | -------------------------------------------------------------------------------- /src/Content/UserDataDisplay.vala: -------------------------------------------------------------------------------- 1 | /* UserDataDisplay.vala 2 | * 3 | * Copyright 2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Displays one UserDataField in UserDisplay. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Content/UserDataDisplay.ui")] 27 | public class UserDataDisplay : Gtk.Widget { 28 | 29 | // UI-Elements of UserDataDisplay 30 | [GtkChild] 31 | private unowned Gtk.Image verified_icon; 32 | [GtkChild] 33 | private unowned Gtk.Label name_label; 34 | [GtkChild] 35 | private unowned Gtk.Label content_label; 36 | 37 | /** 38 | * The UserDataField to be displayed in this widget. 39 | */ 40 | public Backend.UserDataField field { 41 | get { 42 | return displayed_field; 43 | } 44 | construct set { 45 | displayed_field = value; 46 | 47 | // Update the widget with the new values. 48 | name_label.label = displayed_field != null ? displayed_field.name : "(null)"; 49 | content_label.label = displayed_field != null ? displayed_field.content : "(null)"; 50 | 51 | // Update the verified status 52 | bool data_verified = displayed_field != null ? displayed_field.verified != null : false; 53 | string verified_tooltip = data_verified 54 | ? _("Verified on %s").printf (DisplayUtils.display_date (displayed_field.verified)) 55 | : null; 56 | 57 | verified_icon.visible = data_verified; 58 | verified_icon.tooltip_text = verified_tooltip; 59 | name_label.tooltip_text = verified_tooltip; 60 | DisplayUtils.conditional_css (data_verified, this, "verified"); 61 | } 62 | } 63 | 64 | /** 65 | * Constructs a new widget for a field. 66 | * 67 | * @param field The UserDataField to be displayed in this widget. 68 | */ 69 | public UserDataDisplay (Backend.UserDataField field) { 70 | Object ( 71 | field: field, 72 | css_name: "UserDataDisplay" 73 | ); 74 | } 75 | 76 | /** 77 | * Activated when a link in the text is clicked. 78 | */ 79 | [GtkCallback] 80 | private bool on_link_clicked (string uri) { 81 | return DisplayUtils.entities_link_action (uri, this); 82 | } 83 | 84 | /** 85 | * Stores the displayed UserDataField. 86 | */ 87 | private Backend.UserDataField? displayed_field; 88 | 89 | } 90 | -------------------------------------------------------------------------------- /lib/Base/Organization/Thread.vala: -------------------------------------------------------------------------------- 1 | /* Thread.vala 2 | * 3 | * Copyright 2022-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Provides the utilities to display a thread based on a post. 25 | * 26 | * A Thread provides a list for displaying replies around a specified 27 | * "main post". It will display all posts preceding the main post until 28 | * the top one, as well as all replies to the main post. 29 | */ 30 | public abstract class Backend.Thread : Backend.Collection, Backend.PullableCollection, Backend.PostConnections { 31 | 32 | /** 33 | * The session used to pull posts. 34 | */ 35 | public Session session { get; construct; } 36 | 37 | /** 38 | * The post from which the thread is build. 39 | */ 40 | public Post main_post { get; construct; } 41 | 42 | /** 43 | * The id of the newest item in the collection. 44 | */ 45 | protected string? newest_item_id { get; set; default = null; } 46 | 47 | /** 48 | * If the reposted post should be compared instead of the repost. 49 | */ 50 | public override bool check_reposted { get; construct; default = true; } 51 | 52 | /** 53 | * Calls the API to retrieve all items from this Collection. 54 | * 55 | * @throws Error Any error while accessing the API and pulling the items. 56 | */ 57 | public abstract async void pull_items () throws Error; 58 | 59 | /** 60 | * Used to compares two iterators in the list when sorting. 61 | * 62 | * @param a The first iterator to compare. 63 | * @param b The second iterator to compare. 64 | * 65 | * @return How the iterators are sorted (positive when a before b, negative when b before a). 66 | */ 67 | protected override int sort_func (SequenceIter a, SequenceIter b) { 68 | // Use the upmost parent as reference 69 | var post_a = upmost_parent (a); 70 | var post_b = upmost_parent (b); 71 | 72 | assert (post_a.post_type != REPOST); 73 | assert (post_b.post_type != REPOST); 74 | 75 | // Check if posts are connected 76 | if (post_a.replied_to_id == post_b.id) { 77 | return 1; 78 | } 79 | if (post_b.replied_to_id == post_a.id) { 80 | return -1; 81 | } 82 | 83 | // Sort posts by date 84 | DateTime x = post_a.creation_date; 85 | DateTime y = post_b.creation_date; 86 | return x.compare (y); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /tests/Parsing/PostParsing.vala: -------------------------------------------------------------------------------- 1 | /* PostParsing.vala 2 | * 3 | * Copyright 2021-2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Runs different tests on a post. 25 | */ 26 | void run_post_checks (Backend.Post post, Json.Object checks) { 27 | // Check parsed post against check objects. 28 | PostChecks.check_basic_fields (post, checks); 29 | #if DEBUG 30 | PostChecks.check_text_parsing (post, checks); 31 | #endif 32 | PostChecks.check_text_formatting (post, checks); 33 | 34 | // Check post author against check object 35 | Json.Object author_checks = checks.get_object_member ("author"); 36 | UserChecks.check_basic_fields (post.author, author_checks); 37 | } 38 | 39 | /** 40 | * Tests creation of a specific post and runs test on it. 41 | */ 42 | void run_post_test (string module, string post_json, string check_json) { 43 | Json.Object check_object; 44 | Json.Object post_object; 45 | Backend.Post checked_post; 46 | 47 | // Creates a Post object from the post json 48 | check_object = TestUtils.load_json (@"PostData/$(module)/$(check_json)"); 49 | post_object = TestUtils.load_json (@"PostData/$(module)/$(post_json)"); 50 | switch (module) { 51 | #if SUPPORT_MASTODON 52 | case "Mastodon": 53 | checked_post = Backend.Mastodon.Post.from_json (post_object); 54 | break; 55 | #endif 56 | default: 57 | error ("No valid Post could be created!"); 58 | } 59 | 60 | // Run the checks for the post 61 | run_post_checks (checked_post, check_object); 62 | 63 | // FIXME: Tests for referenced posts require an account 64 | } 65 | 66 | /** 67 | * Tests parsing of Post content. 68 | */ 69 | int main (string[] args) { 70 | Test.init (ref args); 71 | 72 | #if SUPPORT_MASTODON 73 | Test.add_func ("/PostParsing/BasicPost/Mastodon", () => { 74 | run_post_test ("Mastodon", "BasicPost.json", "BasicChecks.json"); 75 | }); 76 | Test.add_func ("/PostParsing/EntitiesPost/Mastodon", () => { 77 | run_post_test ("Mastodon", "EntitiesPost.json", "EntitiesChecks.json"); 78 | }); 79 | Test.add_func ("/PostParsing/HashtagsPost/Mastodon", () => { 80 | run_post_test ("Mastodon", "HashtagsPost.json", "HashtagsChecks.json"); 81 | }); 82 | Test.add_func ("/PostParsing/RepostPost/Mastodon", () => { 83 | run_post_test ("Mastodon", "RepostPost.json", "RepostChecks.json"); 84 | }); 85 | #endif 86 | 87 | Test.set_nonfatal_assertions (); 88 | return Test.run (); 89 | } 90 | -------------------------------------------------------------------------------- /src/Widgets/WaitingButton.vala: -------------------------------------------------------------------------------- 1 | /* WaitingButton.vala 2 | * 3 | * Copyright 2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Customized button that contains a Gtk.Spinner to indicated async action. 25 | */ 26 | [GtkTemplate (ui="/uk/co/ibboard/Cawbird/ui/Widgets/WaitingButton.ui")] 27 | public class WaitingButton : Gtk.Widget { 28 | 29 | // UI-Elements of WaitingButton 30 | [GtkChild] 31 | private unowned Gtk.Stack waiting_stack; 32 | [GtkChild] 33 | private unowned Gtk.Box button_content; 34 | [GtkChild] 35 | private unowned Gtk.Image button_icon; 36 | [GtkChild] 37 | private unowned Gtk.Label button_label; 38 | [GtkChild] 39 | private unowned Gtk.Spinner waiting_spinner; 40 | 41 | /** 42 | * If one should wait for an async action. 43 | */ 44 | public bool waiting { 45 | get { 46 | return waiting_spinner.spinning; 47 | } 48 | set { 49 | if (value) { 50 | waiting_stack.set_visible_child (waiting_spinner); 51 | } else { 52 | waiting_stack.set_visible_child (button_content); 53 | } 54 | waiting_spinner.spinning = value; 55 | } 56 | } 57 | 58 | /** 59 | * The label of this button. 60 | */ 61 | public string label { 62 | get { 63 | return internal_label; 64 | } 65 | set { 66 | internal_label = value; 67 | button_label.label = internal_label; 68 | // Hide the Gtk.Label when label is empty 69 | button_label.visible = internal_label != "" && internal_label != null; 70 | } 71 | } 72 | 73 | /** 74 | * The name of an icon for this button. 75 | */ 76 | public string icon_name { 77 | get { 78 | return internal_icon_name; 79 | } 80 | set { 81 | internal_icon_name = value; 82 | button_icon.icon_name = internal_icon_name; 83 | // Hide the Gtk.Label when label is empty 84 | button_icon.visible = internal_icon_name != "" && internal_icon_name != null; 85 | } 86 | } 87 | 88 | /** 89 | * Deconstructs WaitingButton and it's childrens. 90 | */ 91 | public override void dispose () { 92 | // Deconstruct childrens 93 | waiting_stack.unparent (); 94 | base.dispose (); 95 | } 96 | 97 | /** 98 | * Contains the label for this button. 99 | */ 100 | private string? internal_label = null; 101 | 102 | /** 103 | * Contains the icon_name for this button. 104 | */ 105 | private string? internal_icon_name = null; 106 | 107 | } 108 | -------------------------------------------------------------------------------- /lib/Mastodon/Content/User.vala: -------------------------------------------------------------------------------- 1 | /* User.vala 2 | * 3 | * Copyright 2021-2023 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Stores information about one user of a platform. 25 | */ 26 | public class Backend.Mastodon.User : Backend.User { 27 | 28 | /** 29 | * Parses an given Json.Object and creates an User object. 30 | * 31 | * @param json A Json.Object retrieved from the API. 32 | */ 33 | internal User (Json.Object json) { 34 | // Get the url for avatar and header 35 | string avatar_url = json.get_string_member ("avatar_static"); 36 | string header_url = json.get_string_member ("header_static"); 37 | 38 | // Get url and domain to this user 39 | string user_url = json.get_string_member ("url"); 40 | string user_domain = Utils.ParseUtils.strip_domain (user_url); 41 | 42 | // Construct the object with properties 43 | Object ( 44 | // Set the id of the user 45 | id: json.get_string_member ("id"), 46 | 47 | // Set the creation date for the user 48 | creation_date: new DateTime.from_iso8601 ( 49 | json.get_string_member ("created_at"), 50 | new TimeZone.utc () 51 | ), 52 | 53 | // Set the names of the user 54 | display_name: json.get_string_member ("display_name"), 55 | username: json.get_string_member ("acct"), 56 | 57 | // Set the url and domain 58 | url: user_url, 59 | domain: user_domain, 60 | 61 | // Parses the data fields 62 | data_fields: UserDataField.parse_list (json.get_array_member ("fields")), 63 | 64 | // Set metrics 65 | followers_count: (int) json.get_int_member ("followers_count"), 66 | following_count: (int) json.get_int_member ("following_count"), 67 | posts_count: (int) json.get_int_member ("statuses_count"), 68 | 69 | // Set the images 70 | avatar: avatar_url.length > 0 ? Media.from_url (PICTURE, avatar_url) : null, 71 | header: header_url.length > 0 ? Media.from_url (PICTURE, header_url) : null 72 | ); 73 | 74 | // Parse the description into modules 75 | description_modules = Utils.TextParser.instance.parse_text (json.get_string_member ("note")); 76 | 77 | // First format of the description. 78 | description = Backend.Utils.TextUtils.format_text (description_modules); 79 | 80 | // Get possible flags for this user 81 | if (json.get_boolean_member ("locked")) { 82 | flags = flags | MODERATED; 83 | } 84 | if (json.get_boolean_member ("bot")) { 85 | flags = flags | BOT; 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /ui/Media/MediaDisplay.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template MediaDisplay : Gtk.Widget { 5 | width-request: 360; 6 | height-request: 450; 7 | layout-manager: Gtk.BinLayout {}; 8 | 9 | styles [ 10 | "osd" 11 | ] 12 | 13 | Adw.Carousel media_carousel { 14 | page-changed => changed_page (); 15 | } 16 | 17 | Gtk.Revealer top_toolbar { 18 | reveal-child: bind MediaDisplay.display_controls; 19 | transition-type: crossfade; 20 | margin-top: 12; 21 | margin-bottom: 12; 22 | margin-start: 12; 23 | margin-end: 12; 24 | valign: start; 25 | 26 | Gtk.WindowHandle { 27 | Gtk.CenterBox { 28 | styles [ 29 | "toolbar", 30 | "osd" 31 | ] 32 | 33 | [start] 34 | Gtk.Button { 35 | icon-name: "go-previous-symbolic"; 36 | action-name: "window.close"; 37 | } 38 | 39 | [center] 40 | Adw.CarouselIndicatorDots { 41 | carousel: media_carousel; 42 | } 43 | 44 | [end] 45 | Gtk.MenuButton { 46 | icon-name: "view-more-symbolic"; 47 | menu-model: media-options; 48 | } 49 | } 50 | } 51 | } 52 | 53 | Gtk.Revealer previous_control { 54 | reveal-child: bind MediaDisplay.display_controls; 55 | transition-type: crossfade; 56 | margin-top: 12; 57 | margin-bottom: 12; 58 | margin-start: 12; 59 | margin-end: 12; 60 | valign: center; 61 | halign: start; 62 | 63 | Gtk.Button { 64 | action-name: "media_display.select_previous"; 65 | icon-name: "go-previous-symbolic"; 66 | 67 | styles [ 68 | "osd" 69 | ] 70 | } 71 | } 72 | 73 | Gtk.Revealer next_control { 74 | reveal-child: bind MediaDisplay.display_controls; 75 | transition-type: crossfade; 76 | margin-top: 12; 77 | margin-bottom: 12; 78 | margin-start: 12; 79 | margin-end: 12; 80 | valign: center; 81 | halign: end; 82 | 83 | Gtk.Button { 84 | action-name: "media_display.select_next"; 85 | icon-name: "go-next-symbolic"; 86 | 87 | styles [ 88 | "osd" 89 | ] 90 | } 91 | } 92 | 93 | Gtk.Revealer bottom_toolbar { 94 | reveal-child: bind MediaDisplay.display_bottom_bar; 95 | transition-type: crossfade; 96 | margin-top: 12; 97 | margin-bottom: 12; 98 | margin-start: 12; 99 | margin-end: 12; 100 | valign: end; 101 | 102 | Gtk.Box { 103 | orientation: vertical; 104 | 105 | styles [ 106 | "toolbar", 107 | "osd" 108 | ] 109 | 110 | Gtk.Label description_label { 111 | xalign: 0.0; 112 | wrap: true; 113 | } 114 | 115 | Gtk.MediaControls video_controls {} 116 | } 117 | } 118 | } 119 | 120 | menu media-options { 121 | section { 122 | item { 123 | label: _("Save Media"); 124 | action: "media.save-media"; 125 | } 126 | 127 | item { 128 | label: _("Copy Media URL"); 129 | action: "media.copy-url"; 130 | } 131 | 132 | item { 133 | label: _("Open in Browser"); 134 | action: "media.open-url"; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/Parsing/MediaParsing.vala: -------------------------------------------------------------------------------- 1 | /* MediaParsing.vala 2 | * 3 | * Copyright 2021-2022 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * Runs different tests on a media post. 25 | */ 26 | void run_media_checks (Backend.Post post, Json.Object checks) { 27 | // Check parsed post against check objects. 28 | PostChecks.check_basic_fields (post, checks); 29 | #if DEBUG 30 | PostChecks.check_text_parsing (post, checks); 31 | #endif 32 | PostChecks.check_text_formatting (post, checks); 33 | 34 | // Check post author against check object 35 | Json.Object author_checks = checks.get_object_member ("author"); 36 | UserChecks.check_basic_fields (post.author, author_checks); 37 | 38 | // Check attached media against checks 39 | if (checks.has_member ("attached_media")) { 40 | MediaChecks.check_all_media (post, checks); 41 | } 42 | } 43 | 44 | /** 45 | * Tests creation of a specific media post and runs test on it. 46 | */ 47 | void run_media_test (string module, string post_json, string check_json) { 48 | Json.Object check_object; 49 | Json.Object post_object; 50 | Backend.Post checked_post; 51 | 52 | // Creates a Post object from the post json 53 | check_object = TestUtils.load_json (@"MediaData/$(module)/$(check_json)"); 54 | post_object = TestUtils.load_json (@"MediaData/$(module)/$(post_json)"); 55 | switch (module) { 56 | #if SUPPORT_MASTODON 57 | case "Mastodon": 58 | checked_post = Backend.Mastodon.Post.from_json (post_object); 59 | break; 60 | #endif 61 | default: 62 | error ("No valid Post could be created!"); 63 | } 64 | 65 | // Run the checks for the post 66 | run_media_checks (checked_post, check_object); 67 | 68 | // FIXME: Tests for referenced posts require an account 69 | } 70 | 71 | /** 72 | * Tests parsing of Media post content. 73 | */ 74 | int main (string[] args) { 75 | Test.init (ref args); 76 | 77 | #if SUPPORT_MASTODON 78 | Test.add_func ("/MediaParsing/OnePicture/Mastodon", () => { 79 | run_media_test ("Mastodon", "OnePicturePost.json", "OnePictureChecks.json"); 80 | }); 81 | Test.add_func ("/MediaParsing/TwoPicture/Mastodon", () => { 82 | run_media_test ("Mastodon", "TwoPicturePost.json", "TwoPictureChecks.json"); 83 | }); 84 | Test.add_func ("/MediaParsing/ThreePicture/Mastodon", () => { 85 | run_media_test ("Mastodon", "ThreePicturePost.json", "ThreePictureChecks.json"); 86 | }); 87 | Test.add_func ("/MediaParsing/FourPicture/Mastodon", () => { 88 | run_media_test ("Mastodon", "FourPicturePost.json", "FourPictureChecks.json"); 89 | }); 90 | #endif 91 | 92 | Test.set_nonfatal_assertions (); 93 | return Test.run (); 94 | } 95 | -------------------------------------------------------------------------------- /src/Widgets/CroppedPicture.vala: -------------------------------------------------------------------------------- 1 | /* CroppedPicture.vala 2 | * 3 | * Copyright 2021 Frederick Schenk 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | using GLib; 22 | 23 | /** 24 | * A widget that displaying an picture inside it's given bounds. 25 | */ 26 | public class CroppedPicture : Gtk.Widget { 27 | 28 | /** 29 | * The paintable which will be displayed. 30 | */ 31 | public Gdk.Paintable paintable { 32 | get { 33 | return displayed_paintable; 34 | } 35 | set { 36 | displayed_paintable = value; 37 | this.queue_draw (); 38 | } 39 | } 40 | 41 | /** 42 | * If the paintable should be blurred. 43 | */ 44 | public bool blur_paintable { 45 | get { 46 | return blurred; 47 | } 48 | set { 49 | blurred = value; 50 | this.queue_draw (); 51 | } 52 | } 53 | 54 | /** 55 | * Snapshots the widget for display. 56 | */ 57 | public override void snapshot (Gtk.Snapshot snapshot) { 58 | // Stop early when no paintable was given 59 | if (displayed_paintable == null) { 60 | return; 61 | } 62 | 63 | // Get the size of the widget 64 | int width = this.get_width (); 65 | int height = this.get_height (); 66 | 67 | // Get aspect ratios 68 | double widget_ratio = (double) width / height; 69 | double paint_ratio = displayed_paintable.get_intrinsic_aspect_ratio (); 70 | 71 | // Calculate paintable size 72 | double w, h; 73 | if (paint_ratio < widget_ratio) { 74 | w = width; 75 | h = width / paint_ratio; 76 | } else { 77 | w = height * paint_ratio; 78 | h = height; 79 | } 80 | 81 | // Calculate paintable translation 82 | int x = (int) ((width - Math.ceil (w)) / 2); 83 | int y = (int) (Math.floor(height - Math.ceil (h)) / 2); 84 | 85 | // Append the size clip 86 | snapshot.push_clip (Graphene.Rect ().init (0, 0, width, height)); 87 | 88 | // Apply the blur 89 | if (blurred) { 90 | snapshot.push_blur (64.0); 91 | } 92 | 93 | // Snapshot the paintable 94 | snapshot.save (); 95 | snapshot.translate (Graphene.Point ().init (x, y)); 96 | displayed_paintable.snapshot (snapshot, w, h); 97 | snapshot.restore (); 98 | 99 | // Position the pushed effects 100 | snapshot.pop (); 101 | if (blurred) { 102 | snapshot.pop (); 103 | } 104 | } 105 | 106 | /** 107 | * Stores the displayed paintable. 108 | */ 109 | private Gdk.Paintable? displayed_paintable = null; 110 | 111 | /** 112 | * Stores if paintable should be blurred. 113 | */ 114 | private bool blurred = false; 115 | 116 | } 117 | -------------------------------------------------------------------------------- /tests/Parsing/PostData/Mastodon/HashtagsChecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "106962969208581539", 3 | "post_type": "BACKEND_POST_TYPE_NORMAL", 4 | "creation_date": "2021-09-20T08:09:44.671Z", 5 | "text": { 6 | "no_flags": "New post, new test!\nNow, let's test for a #hashtag, but also for some at the end: #hashtag2 #hashtag3 #hashtag4", 7 | "no_trail_tags": "New post, new test!\nNow, let's test for a #hashtag, but also for some at the end:" 8 | }, 9 | "text_modules": [ 10 | { 11 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 12 | "display": "New post, new test!", 13 | "target": null, 14 | "text_start": 0, 15 | "text_end": 19 16 | }, 17 | { 18 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 19 | "display": "\n", 20 | "target": null, 21 | "text_start": 19, 22 | "text_end": 20 23 | }, 24 | { 25 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 26 | "display": "Now, let's test for a ", 27 | "target": null, 28 | "text_start": 20, 29 | "text_end": 42 30 | }, 31 | { 32 | "type": "BACKEND_TEXT_MODULE_TYPE_TAG", 33 | "display": "#hashtag", 34 | "target": "#hashtag", 35 | "text_start": 42, 36 | "text_end": 50 37 | }, 38 | { 39 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 40 | "display": ", but also for some at the end: ", 41 | "target": null, 42 | "text_start": 50, 43 | "text_end": 82 44 | }, 45 | { 46 | "type": "BACKEND_TEXT_MODULE_TYPE_TRAIL_TAG", 47 | "display": "#hashtag2", 48 | "target": "#hashtag2", 49 | "text_start": 82, 50 | "text_end": 91 51 | }, 52 | { 53 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 54 | "display": " ", 55 | "target": null, 56 | "text_start": 91, 57 | "text_end": 92 58 | }, 59 | { 60 | "type": "BACKEND_TEXT_MODULE_TYPE_TRAIL_TAG", 61 | "display": "#hashtag3", 62 | "target": "#hashtag3", 63 | "text_start": 92, 64 | "text_end": 101 65 | }, 66 | { 67 | "type": "BACKEND_TEXT_MODULE_TYPE_TEXT", 68 | "display": " ", 69 | "target": null, 70 | "text_start": 101, 71 | "text_end": 102 72 | }, 73 | { 74 | "type": "BACKEND_TEXT_MODULE_TYPE_TRAIL_TAG", 75 | "display": "#hashtag4", 76 | "target": "#hashtag4", 77 | "text_start": 102, 78 | "text_end": 111 79 | } 80 | ], 81 | "author": { 82 | "id": "106866867257179166", 83 | "display_name": "CawbirdTestAccount", 84 | "username": "cawbird_test_account", 85 | "avatar_url": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png" 86 | }, 87 | "domain": "mastodon.social", 88 | "url": "https://mastodon.social/@cawbird_test_account/106962969208581539", 89 | "source": "Web", 90 | "liked_count": 0, 91 | "replied_count": 0, 92 | "reposted_count": 0 93 | } 94 | -------------------------------------------------------------------------------- /lib/Base/Utils/TextFormats.vala: -------------------------------------------------------------------------------- 1 | /* TextFormats.vala 2 | * 3 | * Copyright 2022 CodedOre <47981497+CodedOre@users.noreply.github.com> 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | /** 22 | * Various settings for text formatting. 23 | */ 24 | [Flags] 25 | public enum Backend.Utils.FormatFlag { 26 | 27 | /** 28 | * Hide hashtags after the text. 29 | */ 30 | HIDE_TRAILING_TAGS, 31 | 32 | /** 33 | * Display links leading to quotes. 34 | */ 35 | SHOW_QUOTE_LINKS, 36 | 37 | /** 38 | * Display links leading to media. 39 | */ 40 | SHOW_MEDIA_LINKS 41 | 42 | } 43 | 44 | /** 45 | * Allows to set text formatting. 46 | * 47 | * This singleton provides access to methods to set 48 | * the flags that determine the formatting of 49 | * text from Post and description from Profile. 50 | */ 51 | [SingleInstance] 52 | public class Backend.Utils.TextFormats : Object { 53 | 54 | /** 55 | * Signals a changed format setting. 56 | * 57 | * Used by Post and Profile to regenerate their text properties. 58 | */ 59 | internal signal void update_formatting (); 60 | 61 | /** 62 | * The global instance of TextFormats. 63 | */ 64 | internal static TextFormats instance { 65 | get { 66 | if (global_instance == null) { 67 | global_instance = new TextFormats (); 68 | } 69 | return global_instance; 70 | } 71 | } 72 | 73 | /** 74 | * Checks if a certain flag for text formatting is set. 75 | * 76 | * @param flag The flag to be checked. 77 | * 78 | * @return A boolean if the flag is set. 79 | */ 80 | public static bool get_format_flag (FormatFlag flag) { 81 | return flag in instance.format_flags; 82 | } 83 | 84 | /** 85 | * Sets a flag for text formatting to a certain value. 86 | * 87 | * @param flag The flag to be set. 88 | * @param setting If the flag should be enabled or not. 89 | */ 90 | public static void set_format_flag (FormatFlag flag, bool setting) { 91 | // Return if setting wouldn't change 92 | if (setting == get_format_flag (flag)) { 93 | return; 94 | } 95 | 96 | // Apply the flag 97 | if (setting) { 98 | instance.format_flags = instance.format_flags | flag; 99 | } else { 100 | instance.format_flags = instance.format_flags & ~flag; 101 | } 102 | 103 | // Signalize the update 104 | instance.update_formatting (); 105 | } 106 | 107 | /** 108 | * Stores the global instance of TextFormats. 109 | * 110 | * Only access over the instance property! 111 | */ 112 | private static TextFormats? global_instance = null; 113 | 114 | /** 115 | * Settings for the formatting of text. 116 | */ 117 | private FormatFlag format_flags = 0; 118 | 119 | } 120 | -------------------------------------------------------------------------------- /tests/Parsing/PostData/Mastodon/HashtagsPost.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "106962969208581539", 3 | "created_at": "2021-09-20T08:09:44.671Z", 4 | "in_reply_to_id": null, 5 | "in_reply_to_account_id": null, 6 | "sensitive": false, 7 | "spoiler_text": "", 8 | "visibility": "public", 9 | "language": "en", 10 | "uri": "https://mastodon.social/users/cawbird_test_account/statuses/106962969208581539", 11 | "url": "https://mastodon.social/@cawbird_test_account/106962969208581539", 12 | "replies_count": 0, 13 | "reblogs_count": 0, 14 | "favourites_count": 0, 15 | "edited_at": null, 16 | "content": "

New post, new test!
Now, let's test for a #hashtag, but also for some at the end: #hashtag2 #hashtag3 #hashtag4

", 17 | "reblog": null, 18 | "application": { 19 | "name": "Web", 20 | "website": null 21 | }, 22 | "account": { 23 | "id": "106866867257179166", 24 | "username": "cawbird_test_account", 25 | "acct": "cawbird_test_account", 26 | "display_name": "CawbirdTestAccount", 27 | "locked": false, 28 | "bot": false, 29 | "discoverable": false, 30 | "group": false, 31 | "created_at": "2021-09-03T00:00:00.000Z", 32 | "note": "

This may or may not be a account from @CodedOre for testing some API's for a certain Client.

", 33 | "url": "https://mastodon.social/@cawbird_test_account", 34 | "avatar": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png", 35 | "avatar_static": "https://files.mastodon.social/accounts/avatars/106/866/867/257/179/166/original/6bf23bd3fb731624.png", 36 | "header": "https://files.mastodon.social/accounts/headers/106/866/867/257/179/166/original/1b43374005561eec.jpeg", 37 | "header_static": "https://files.mastodon.social/accounts/headers/106/866/867/257/179/166/original/1b43374005561eec.jpeg", 38 | "followers_count": 3, 39 | "following_count": 0, 40 | "statuses_count": 11, 41 | "last_status_at": "2021-12-20", 42 | "emojis": [], 43 | "fields": [ 44 | { 45 | "name": "Lives in", 46 | "value": "org.gnome.Sdk", 47 | "verified_at": null 48 | }, 49 | { 50 | "name": "Example Weblink", 51 | "value": "https://example.com", 52 | "verified_at": null 53 | } 54 | ] 55 | }, 56 | "media_attachments": [], 57 | "mentions": [], 58 | "tags": [ 59 | { 60 | "name": "hashtag2", 61 | "url": "https://mastodon.social/tags/hashtag2" 62 | }, 63 | { 64 | "name": "hashtag3", 65 | "url": "https://mastodon.social/tags/hashtag3" 66 | }, 67 | { 68 | "name": "hashtag4", 69 | "url": "https://mastodon.social/tags/hashtag4" 70 | }, 71 | { 72 | "name": "hashtag", 73 | "url": "https://mastodon.social/tags/hashtag" 74 | } 75 | ], 76 | "emojis": [], 77 | "card": null, 78 | "poll": null 79 | } 80 | --------------------------------------------------------------------------------