├── .coveragerc
├── .gitignore
├── .travis.yml
├── COPYING
├── ChangeLog
├── ChangeLog.md
├── Makefile.am
├── NEWS
├── README
├── README.md
├── app.nix
├── autogen.sh
├── configure.ac
├── data
├── Makefile.am
├── app-menu.ui
├── empty-view.ui
├── icons
│ ├── reddit-detach-symbolic.svg
│ ├── reddit-detach-symbolic.symbolic.png
│ ├── reddit-downvote-symbolic.svg
│ ├── reddit-downvote-symbolic.symbolic.png
│ ├── reddit-mail-reply-all-symbolic.svg
│ ├── reddit-mail-reply-all-symbolic.symbolic.png
│ ├── reddit-mail-reply-sender-symbolic.svg
│ ├── reddit-mail-reply-sender-symbolic.symbolic.png
│ ├── reddit-novote-symbolic.svg
│ ├── reddit-novote-symbolic.symbolic.png
│ ├── reddit-upvote-symbolic.svg
│ ├── reddit-upvote-symbolic.symbolic.png
│ ├── today.sam.reddit-is-gtk-symbolic.svg
│ └── today.sam.reddit-is-gtk.svg
├── mascot.svg
├── post-top-bar.ui
├── reddit-is-gtk.appdata.xml.in
├── reddit-is-gtk.desktop.in
├── reddit-is-gtk.gresource.xml
├── row-comment.ui
├── row-link.ui
├── settings-window.ui
├── shortcuts-window.ui
├── style.css
├── submit-window.ui
├── subreddit-about.ui
├── today.sam.something-for-reddit.gschema.xml
├── user-about.ui
└── webview-toolbar.ui
├── debian
├── changelog
├── compat
├── control
├── copyright
├── rules
└── source
│ └── format
├── default.nix
├── dev-shell.nix
├── fedora
└── fedora.spec
├── po
├── .intltool-merge-cache
├── POTFILES
├── POTFILES.in
└── POTFILES.skip
├── publish-flatpak.md
├── reddit-is-gtk.in
├── redditisgtk
├── Makefile.am
├── __init__.py
├── aboutrow.py
├── api.py
├── buttons.py
├── comments.py
├── conftest.py
├── emptyview.py
├── gtktestutil.py
├── gtkutil.py
├── identity.py
├── identitybutton.py
├── main.py
├── mediapreview.py
├── newmarkdown.py
├── palettebutton.py
├── posttopbar.py
├── readcontroller.py
├── settings.py
├── subentry.py
├── sublist.py
├── sublistrows.py
├── submit.py
├── test_aboutrow.py
├── test_api.py
├── test_comments.py
├── test_emptyview.py
├── test_gtkutil.py
├── test_identity.py
├── test_identitybutton.py
├── test_newmarkdown.py
├── test_posttopbar.py
├── test_readcontroller.py
├── test_subentry.py
├── test_sublist.py
├── test_sublistrows.py
├── test_submit.py
├── tests-data
│ ├── api__load-more.json
│ ├── comments--thread.json
│ ├── identity--load.json
│ ├── posttopbar--comment.json
│ ├── posttopbar--post.json
│ ├── snapshots
│ │ ├── newmarkdown--code.json
│ │ ├── newmarkdown--error-handler.json
│ │ ├── newmarkdown--escaping-amp.json
│ │ ├── newmarkdown--escaping-code.json
│ │ ├── newmarkdown--escaping.json
│ │ ├── newmarkdown--heading.json
│ │ ├── newmarkdown--hello.json
│ │ ├── newmarkdown--hr.json
│ │ ├── newmarkdown--inline.json
│ │ ├── newmarkdown--link--r.json
│ │ ├── newmarkdown--list.json
│ │ ├── newmarkdown--quote.json
│ │ ├── newmarkdown--super-long.json
│ │ └── newmarkdown--super.json
│ ├── sublistrows--pm.json
│ ├── sublistrows--thumb-from-previews.json
│ ├── submit--good-response.json
│ └── submit--ratelimit-response.json
└── webviews.py
├── screenshots
├── 0.2.1-askreddit.png
├── 0.2.1-dankmemes.png
└── 0.2.1-dark.png
└── today.sam.reddit-is-gtk.json
/.coveragerc:
--------------------------------------------------------------------------------
1 | # .coveragerc to control coverage.py
2 | [report]
3 | # Regexes for lines to exclude from consideration
4 | exclude_lines =
5 | # Have to re-enable the standard pragma
6 | pragma: no cover
7 |
8 | # Don't complain about missing debug-only code:
9 | def __repr__
10 | if self\.debug
11 | if DEBUG
12 |
13 | # Don't worry about unrunnable code:
14 | if __name__ == '__main__':
15 |
16 | # Don't complain if tests don't hit defensive assertion code:
17 | raise AssertionError
18 | raise NotImplementedError
19 |
20 | # Regexs for files to exclude, namely the test files themselves
21 | omit =
22 | **/test_*.py
23 | **/conftest.py
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | /*.bak
3 | /*.lo
4 | /*.o
5 | /*.orig
6 | /*.rej
7 | /*.tab.c
8 | /*~
9 | .*.sw[nop]
10 | /.deps
11 | /.dirstamp
12 | /.libs
13 | /AUTHORS
14 | /GPATH
15 | /GRTAGS
16 | /GSYMS
17 | /GTAGS
18 | /ID
19 | /INSTALL
20 | /Makefile
21 | /Makefile.in
22 | /TAGS
23 | /_libs
24 | /aclocal.m4
25 | /autom4te.cache
26 | /autoscan.log
27 | /compile
28 | /config.cache
29 | /config.guess
30 | /config.h
31 | /config.h.in
32 | /config.log
33 | /config.lt
34 | /config.status
35 | /config.status.lineno
36 | /config.sub
37 | /configure
38 | /configure.lineno
39 | /configure.scan
40 | /depcomp
41 | /install-sh
42 | /intltool-extract.in
43 | /intltool-merge.in
44 | /intltool-update.in
45 | /libtool
46 | /ltmain.sh
47 | /m4
48 | /missing
49 | /mkinstalldirs
50 | /so_locations
51 | /stamp-h1
52 | /tags
53 | /py-compile
54 | *.tar.xz
55 | *.desktop
56 | *.gresource
57 | *.compiled
58 | *.patch
59 | *.pyc
60 | Makefile
61 | Makefile.in
62 | Makefile.in.in
63 | /po/stamp-it
64 | reddit-is-gtk
65 | *.valid
66 | Makefile.in
67 | /data/reddit-is-gtk.appdata.xml
68 | /data/.sass-cache
69 | /data/style.css.out
70 | /data/style.dark.css.out
71 | /data/style.css.out.map
72 | /debian/autoreconf.*
73 | /debian/files
74 | /debian/reddit-is-gtk.debhelper.log
75 | /debian/reddit-is-gtk.substvars
76 | /__build_prefix
77 | /.pytest_cache/
78 | .coverage
79 | .flatpak-builder
80 | build-dir
81 | flatpak-repo
82 | htmlcov
83 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | env:
2 | global:
3 | - CC_TEST_REPORTER_ID=ad7d02c4a09f9f9845a84380473e215749292d230080f259395093b25a2b04a4
4 | language: nix
5 | before_script:
6 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
7 | - chmod +x ./cc-test-reporter
8 | - ./cc-test-reporter before-build
9 | script:
10 | - nix-shell dev-shell.nix --command 'echo Downloaded deps'
11 | - xvfb-run nix-shell dev-shell.nix --command './autogen.sh --prefix=$PREFIX && make && make install && pytest --cov=redditisgtk --cov-report xml'
12 | after_script:
13 | - sudo apt-get update
14 | - sudo apt-get install -y python3-dev gcc python3-pip
15 | - sudo pip3 install coverage
16 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT -t coverage.py
17 |
--------------------------------------------------------------------------------
/ChangeLog:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/ChangeLog
--------------------------------------------------------------------------------
/ChangeLog.md:
--------------------------------------------------------------------------------
1 | # 0.2.2 - “The Bugfix Release ⓇⒺⒹⓊⓍ”
2 |
3 | 26/Nov/2018
4 |
5 | - Fixes sign in issues on newer versions of webkit (#61, #58)
6 |
7 | # 0.2.1 - “The Bugfix Release”
8 |
9 | 25/Nov/2018
10 |
11 | It has been great to come back and revitalize this app after 2 years of
12 | neglect. I'm excited about how far this app can be improved, and think this
13 | release makes a good stepping stone towards future features and improvements.
14 |
15 | ## Added
16 |
17 | - The frontpage is now visible. You can see it by going to `/` in the url bar.
18 | (#56)
19 | - The app now supports Flatpak & NixOS installation
20 | - Performance improvements in the comments view
21 | - The codebase is now tested! I'm pretty sure this is one of the only GTK+
22 | apps which has proper UI tests - so this should stop more regressions in the
23 | future!
24 |
25 | ## Changed
26 |
27 | - Replaced the markdown engine, should now support more features (#66)
28 | - The alignment in subreddit listings should now be more uniform. (#64)
29 | - Many size related bugs have been fixed (#6, #47, #72)
30 | - The url bar is now less lenient on formatting. Previously, going to `/linux`
31 | would take you to `/r/linux`. Due to the frontpage changes, this no longer
32 | happens
33 | - [PACKAGING] Moved icons from pixmaps to the hicolor directory
34 | - [PACKAGING] Change SCSS compiler from ruby sass to sassc
35 |
--------------------------------------------------------------------------------
/Makefile.am:
--------------------------------------------------------------------------------
1 | ACLOCAL_AMFLAGS = -I m4
2 | NULL =
3 |
4 | bin_SCRIPTS = reddit-is-gtk
5 |
6 | SUBDIRS = redditisgtk data po
7 |
8 | EXTRA_DIST = \
9 | reddit-is-gtk.in \
10 | $(NULL)
11 |
12 | CLEANFILES = \
13 | $(bin_SCRIPTS)
14 | $(NULL)
15 |
16 | MAINTAINERCLEANFILES = \
17 | $(srcdir)/INSTALL \
18 | $(srcdir)/aclocal.m4 \
19 | $(srcdir)/autoscan.log \
20 | $(srcdir)/compile \
21 | $(srcdir)/config.guess \
22 | $(srcdir)/config.h.in \
23 | $(srcdir)/config.sub \
24 | $(srcdir)/configure.scan \
25 | $(srcdir)/depcomp \
26 | $(srcdir)/install-sh \
27 | $(srcdir)/ltmain.sh \
28 | $(srcdir)/missing \
29 | $(srcdir)/mkinstalldirs \
30 | $(NULL)
31 |
32 | GITIGNOREFILES = \
33 | m4 \
34 | $(NULL)
35 |
36 | reddit-is-gtk: reddit-is-gtk.in Makefile
37 | $(AM_V_GEN)sed \
38 | -e s!\@srcdir\@!$(abs_top_srcdir)! \
39 | -e s!\@prefix\@!$(prefix)! \
40 | -e s!\@datadir\@!$(datadir)! \
41 | -e s!\@pkgdatadir\@!$(pkgdatadir)! \
42 | -e s!\@libexecdir\@!$(libexecdir)! \
43 | -e s!\@libdir\@!$(libdir)! \
44 | -e s!\@pkglibdir\@!$(pkglibdir)! \
45 | -e s!\@localedir\@!$(localedir)! \
46 | -e s!\@pythondir\@!$(pythondir)! \
47 | -e s!\@pyexecdir\@!$(pyexecdir)! \
48 | -e s!\@PACKAGE\@!$(PACKAGE)! \
49 | -e s!\@VERSION\@!$(VERSION)! \
50 | < $< > $@
51 | chmod a+x $@
52 |
53 | all-local: reddit-is-gtk
54 |
55 | -include $(top_srcdir)/git.mk
56 |
57 |
--------------------------------------------------------------------------------
/NEWS:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/NEWS
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/README
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Something For Reddit
2 |
3 | [](https://travis-ci.org/samdroid-apps/something-for-reddit)[](https://codeclimate.com/github/samdroid-apps/something-for-reddit/maintainability)[](https://codeclimate.com/github/samdroid-apps/something-for-reddit/test_coverage)
4 |
5 | A simple Reddit client for GNOME, built for touch, mouse and VIM keyboards.
6 |
7 | 
8 |
9 | 
10 |
11 | 
12 |
13 | # Features
14 |
15 | * Touchscreen tested interface
16 | * VIM style keybindings
17 | * View subreddits, comments and user pages
18 | * Vote on comments and links, write replies
19 | * Integrated WebKit2 browser for viewing links
20 | * Multi-account support
21 |
22 | # Packages
23 |
24 | Up to date:
25 |
26 | | Distro | Command | Info |
27 | |--------|---------|------|
28 | | Flatpak | `flatpak install https://flatpak.dl.sam.today/today.sam.reddit-is-gtk.flatpakref` | |
29 | | NixOS | Run `nix-shell --command reddit-is-gtk` inside the git repo | see `app.nix` for package |
30 | | openSUSE | | https://software.opensuse.org/package/something-for-reddit |
31 |
32 | Being updated (feel free to contribute):
33 |
34 | | Distro | Command | Info |
35 | |--------|---------|------|
36 | | Fedora | `dnf copr enable samtoday/something-for-reddit; dnf install something-for-reddit` | https://copr.fedorainfracloud.org/coprs/samtoday/something-for-reddit/ |
37 | | Archlinux | `yaourt -S something-for-reddit-git` | https://aur.archlinux.org/packages/something-for-reddit-git/ |
38 |
39 | # Installing
40 |
41 | I did this ages ago, so I don't really remember.
42 |
43 | 1. Install `gnome-common` (and autotools, etc.)
44 | 2. Install the `python3-arrow` and `python3-markdown`
45 | 3. Install the `sassc` (from your package manager)
46 |
47 | Then you can just install it like any usual program.
48 |
49 | 1. Download the source code (eg. `git clone https://github.com/samdroid-apps/something-for-reddit; cd something-for-reddit`)
50 | 2. `./autogen.sh`
51 | 3. `make`
52 | 4. `sudo make install`
53 |
54 | There is a .desktop file, but it is also `reddit-is-gtk` command
55 |
56 | Please report the bugs or deficiencies that you find via Github Issues.
57 |
58 | # Development
59 |
60 | A development shell using Nix is provided in `dev-shell.nix`. You can run it
61 | with the following command:
62 |
63 | ```sh
64 | nix-shell dev-shell.nix
65 | ```
66 |
67 | This will include instructions to run the app.
68 |
69 | # Flatpak
70 |
71 | You can build the flatpak with the following commands:
72 |
73 | ```sh
74 | flatpak-builder build-dir today.sam.reddit-is-gtk.json --force-clean --install
75 | ```
76 |
77 | To build the from the local copy of your source, use change the sources option
78 | in the flatpak to as follows:
79 |
80 | ```json
81 | "sources": [
82 | {
83 | "type": "dir",
84 | "path": ".",
85 | "skip": [
86 | "__build_prefix",
87 | ".flatpak-builder",
88 | "flatpak-repo"
89 | ]
90 | }
91 | ]
92 | ```
93 |
94 | # Outdated Roadmap
95 |
96 | Feel free to contribute and do whatever you want.
97 |
98 | Please try and use Flake8!
99 |
100 | * **Any app icon suggestions/designs are appreciated**
101 | - The current one isn't great at all
102 | * Replace the media previews, integrate them with the comments view
103 | * Use gettext
104 | - **If you are interested in translateing this app, please email me!**
105 | * Search all the subreddits on reddit
106 | * Manage subreddits dialog
107 | * Better handle private messages
108 | * Multireddits in the urlbar list
109 | * Mutlireddit add/remove subreddits
110 |
111 | Long Term
112 |
113 | * Optimise the comments view performance
114 | * Separate the reddit json parsing from the view components
115 | * Support other sites (eg. hackernews)
116 |
--------------------------------------------------------------------------------
/app.nix:
--------------------------------------------------------------------------------
1 | { stdenv
2 | , intltool
3 | , gdk_pixbuf
4 | , python3
5 | , pkgconfig
6 | , gtk3
7 | , glib
8 | , hicolor_icon_theme
9 | , makeWrapper
10 | , itstool
11 | , gnome3
12 | , autoconf
13 | , pango
14 | , atk
15 | , sassc }:
16 |
17 | let
18 | py = python3;
19 | # VERSION:
20 | version = "0.2.2";
21 | in
22 | stdenv.mkDerivation rec {
23 | name = "something-for-reddit-${version}";
24 |
25 | src = ./.;
26 |
27 | propagatedUserEnvPkgs = [ gnome3.gnome_themes_standard ];
28 | nativeBuildInputs = [
29 | pkgconfig
30 | autoconf
31 | gnome3.gnome-common
32 | intltool
33 | itstool
34 | sassc
35 | py.pkgs.pytest
36 | py.pkgs.pytestcov
37 | ];
38 |
39 | buildInputs = [
40 | gtk3
41 | glib
42 | gdk_pixbuf
43 | pango
44 | py
45 | hicolor_icon_theme
46 | gnome3.gsettings_desktop_schemas
47 | makeWrapper
48 | gnome3.webkitgtk
49 | gnome3.libsoup
50 | ] ++ (with py.pkgs; [
51 | pygobject3
52 | markdown
53 | arrow
54 | ]);
55 |
56 | preConfigure = ''
57 | ./autogen.sh
58 | '';
59 |
60 | extraLibs = [
61 | gnome3.webkitgtk
62 | gnome3.libsoup
63 | gtk3
64 | glib
65 | pango
66 | # full output for the gi typelib files:
67 | pango.out
68 | atk
69 | gdk_pixbuf
70 | ];
71 | extraTypelibPath = let
72 | paths = map (lib: "${lib}/lib/girepository-1.0/") extraLibs;
73 | in
74 | builtins.concatStringsSep ":" paths;
75 | extraLibPath = stdenv.lib.makeLibraryPath extraLibs;
76 |
77 | preFixup = ''
78 | wrapProgram "$out/bin/reddit-is-gtk" \
79 | --prefix XDG_DATA_DIRS : "$out/share:${gnome3.gnome_themes_standard}/share:$XDG_ICON_DIRS:$GSETTINGS_SCHEMAS_PATH" \
80 | --prefix GI_TYPELIB_PATH : "${extraTypelibPath}:$GI_TYPELIB_PATH" \
81 | --prefix LD_LIBRARY_PATH : "${extraLibPath}" \
82 | --prefix PYTHONPATH : "$PYTHONPATH"
83 | '';
84 |
85 | meta = with stdenv.lib; {
86 | homepage = https://github.com/samdroid-apps/something-for-reddit;
87 | description = "A Reddit Client For GNOME (with Gtk+ and Python)";
88 | maintainers = with maintainers; [ samdroid-apps ];
89 | license = licenses.gpl3;
90 | platforms = platforms.linux;
91 | };
92 | }
93 |
--------------------------------------------------------------------------------
/autogen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Run this to generate all the initial makefiles, etc.
3 |
4 | srcdir=`dirname $0`
5 | test -z "$srcdir" && srcdir=.
6 |
7 | PKG_NAME="reddit-is-gtk"
8 |
9 | test -f $srcdir/configure.ac || {
10 | echo -n "**Error**: Directory "\`$srcdir\'" does not look like the"
11 | echo " top-level reddit-is-gtk directory"
12 | exit 1
13 | }
14 |
15 | which gnome-autogen.sh || {
16 | echo "You need to install gnome-common from GNOME Git (or from"
17 | echo "your OS vendor's package manager)."
18 | exit 1
19 | }
20 |
21 | (cd "$srcdir" ;
22 | test -d m4 || mkdir m4/ ;
23 | git submodule update --init --recursive ;
24 | )
25 | touch AUTHORS
26 | . gnome-autogen.sh
27 |
--------------------------------------------------------------------------------
/configure.ac:
--------------------------------------------------------------------------------
1 | AC_PREREQ(2.63)
2 | # VERSION:
3 | AC_INIT([something-for-reddit],
4 | [0.2.2],
5 | [https://github.com/samdroid-apps/something-for-reddit/issues],
6 | [something-for-reddit],
7 | [https://github.com/samdroid-apps/something-for-reddit])
8 | AC_CONFIG_MACRO_DIR([m4])
9 | AC_CONFIG_SRCDIR([Makefile.am])
10 | AC_CONFIG_HEADERS(config.h)
11 | AM_INIT_AUTOMAKE([1.11 tar-ustar dist-xz no-dist-gzip -Wno-portability subdir-objects])
12 | AM_MAINTAINER_MODE([enable])
13 | m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])])
14 |
15 | AM_PATH_PYTHON([3.3])
16 |
17 | AC_PROG_CC
18 | AM_PROG_CC_C_O
19 | # LT_INIT([disable-static])
20 |
21 | PKG_PROG_PKG_CONFIG([0.22])
22 |
23 | GETTEXT_PACKAGE=reddit-is-gtk
24 | AC_SUBST(GETTEXT_PACKAGE)
25 | AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE, "$GETTEXT_PACKAGE",
26 | [The prefix for our gettext translation domains.])
27 | IT_PROG_INTLTOOL(0.26)
28 |
29 | # I don't know how to get this working on Nix
30 | # TODO: uncomment the following once build works
31 | # AX_PYTHON_MODULE([arrow], [For date humanization])
32 | # AX_PYTHON_MODULE([markdown], [For reddit markdown])
33 |
34 | GLIB_GSETTINGS
35 | # TODO: get this working on Nix
36 | # GOBJECT_INTROSPECTION_REQUIRE([1.35.9])
37 | PKG_CHECK_MODULES([GTK], [gtk+-3.0 >= 3.13.2])
38 | PKG_CHECK_MODULES([LIBSOUP], [libsoup-2.4 >= 2.4])
39 | # TODO: get this working on Nix
40 | # AX_PATH_PROG([sass], [For compiling style sheets])
41 |
42 | GLIB_COMPILE_RESOURCES=`$PKG_CONFIG --variable glib_compile_resources gio-2.0`
43 | AC_SUBST(GLIB_COMPILE_RESOURCES)
44 |
45 | AC_CONFIG_FILES([
46 | Makefile
47 | data/Makefile
48 | redditisgtk/Makefile
49 | po/Makefile.in
50 | ])
51 | AC_OUTPUT
52 |
--------------------------------------------------------------------------------
/data/Makefile.am:
--------------------------------------------------------------------------------
1 | style.dark.css.out: style.css
2 | $(AM_V_GEN) echo '$$dark: true;' | cat - style.css | sassc -s style.dark.css.out
3 |
4 | style.css.out: style.css
5 | $(AM_V_GEN) echo '$$dark: false;' | cat - style.css | sassc -s style.css.out
6 |
7 | resource_files = $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=$(srcdir) --sourcedir=$(builddir) --generate-dependencies $(builddir)/reddit-is-gtk.gresource.xml)
8 | reddit-is-gtk.gresource: reddit-is-gtk.gresource.xml style.css.out style.dark.css.out $(resource_files)
9 | $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) --target=$@ --sourcedir=$(srcdir) --sourcedir=$(builddir) $<
10 |
11 | resourcedir = $(pkgdatadir)
12 | resource_DATA = reddit-is-gtk.gresource
13 |
14 | appsdir = $(datadir)/applications
15 | apps_DATA = reddit-is-gtk.desktop
16 |
17 | @INTLTOOL_DESKTOP_RULE@
18 |
19 | appdatadir = $(datadir)/appdata
20 | appdata_DATA = $(appdata_in_files:.xml.in=.xml)
21 | appdata_in_files = reddit-is-gtk.appdata.xml.in
22 |
23 | metainfodir = $(datadir)/metainfo
24 | metainfo_DATA = reddit-is-gtk.appdata.xml
25 |
26 | @INTLTOOL_XML_RULE@
27 |
28 | hicolor_app_iconsdir = $(datadir)/icons/hicolor/scalable/apps
29 | hicolor_app_icons_DATA = icons/today.sam.reddit-is-gtk.svg
30 | hicolor_app_icons_symbolicdir = $(datadir)/icons/hicolor/symbolic/apps
31 | hicolor_app_icons_symbolic_DATA = icons/today.sam.reddit-is-gtk-symbolic.svg
32 | hicolor_icons_symbolicdir = $(datadir)/icons/hicolor/scalable/actions
33 | hicolor_icons_symbolic_DATA = \
34 | icons/reddit-mail-reply-all-symbolic.svg \
35 | icons/reddit-mail-reply-sender-symbolic.svg \
36 | icons/reddit-detach-symbolic.svg \
37 | icons/reddit-upvote-symbolic.svg \
38 | icons/reddit-novote-symbolic.svg \
39 | icons/reddit-downvote-symbolic.svg
40 | # FIXME: flatpak doesn't seem to support SVG icons at this point, god knows why
41 | hicolor_icons_24dir = $(datadir)/icons/hicolor/24x24/actions
42 | hicolor_icons_24_DATA = \
43 | icons/reddit-mail-reply-all-symbolic.symbolic.png \
44 | icons/reddit-mail-reply-sender-symbolic.symbolic.png \
45 | icons/reddit-detach-symbolic.symbolic.png \
46 | icons/reddit-upvote-symbolic.symbolic.png \
47 | icons/reddit-novote-symbolic.symbolic.png \
48 | icons/reddit-downvote-symbolic.symbolic.png
49 | hicolor_icon_files = \
50 | $(hicolor_app_icons_DATA) \
51 | $(hicolor_app_icons_symbolic_DATA) \
52 | $(hicolor_icons_symbolic_DATA) \
53 | $(hicolor_icons_24_DATA)
54 |
55 | install-data-hook: update-icon-cache
56 | uninstall-hook: update-icon-cache
57 | update-icon-cache:
58 | @-if test -z "$(DESTDIR)"; then \
59 | echo "Updating Gtk hicolor icon cache."; \
60 | $(gtk_update_hicolor_icon_cache); \
61 | else \
62 | echo "*** Icon cache not updated. After (un)install, run this:"; \
63 | echo "*** $(gtk_update_hicolor_icon_cache)"; \
64 | fi
65 |
66 |
67 | gsettings_SCHEMAS = today.sam.something-for-reddit.gschema.xml
68 |
69 | # For uninstalled use
70 | gschemas.compiled: $(gsettings_SCHEMAS) Makefile
71 | $(AM_V_GEN) $(GLIB_COMPILE_SCHEMAS) $(builddir)
72 |
73 | @GSETTINGS_RULES@
74 |
75 | EXTRA_DIST = \
76 | $(resource_files) \
77 | $(hicolor_icon_files) \
78 | reddit-is-gtk.desktop.in \
79 | reddit-is-gtk.appdata.xml.in \
80 | reddit-is-gtk.gresource.xml \
81 | today.sam.something-for-reddit.gschema.xml \
82 | style.css \
83 | $(NULL)
84 |
85 | CLEANFILES = \
86 | reddit-is-gtk.gresource \
87 | reddit-is-gtk.appdata.xml \
88 | $(apps_DATA) \
89 | *.valid \
90 | gschemas.compiled \
91 | style.css.out \
92 | style.css.out.map \
93 | $(NULL)
94 |
--------------------------------------------------------------------------------
/data/app-menu.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
30 |
31 |
--------------------------------------------------------------------------------
/data/empty-view.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
58 |
59 |
--------------------------------------------------------------------------------
/data/icons/reddit-detach-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
116 |
--------------------------------------------------------------------------------
/data/icons/reddit-detach-symbolic.symbolic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/data/icons/reddit-detach-symbolic.symbolic.png
--------------------------------------------------------------------------------
/data/icons/reddit-downvote-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
133 |
--------------------------------------------------------------------------------
/data/icons/reddit-downvote-symbolic.symbolic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/data/icons/reddit-downvote-symbolic.symbolic.png
--------------------------------------------------------------------------------
/data/icons/reddit-mail-reply-all-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
55 |
--------------------------------------------------------------------------------
/data/icons/reddit-mail-reply-all-symbolic.symbolic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/data/icons/reddit-mail-reply-all-symbolic.symbolic.png
--------------------------------------------------------------------------------
/data/icons/reddit-mail-reply-sender-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/data/icons/reddit-mail-reply-sender-symbolic.symbolic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/data/icons/reddit-mail-reply-sender-symbolic.symbolic.png
--------------------------------------------------------------------------------
/data/icons/reddit-novote-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
136 |
--------------------------------------------------------------------------------
/data/icons/reddit-novote-symbolic.symbolic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/data/icons/reddit-novote-symbolic.symbolic.png
--------------------------------------------------------------------------------
/data/icons/reddit-upvote-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
138 |
--------------------------------------------------------------------------------
/data/icons/reddit-upvote-symbolic.symbolic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/data/icons/reddit-upvote-symbolic.symbolic.png
--------------------------------------------------------------------------------
/data/post-top-bar.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | True
8 | False
9 |
10 |
11 | True
12 | True
13 | True
14 |
15 |
16 |
17 | True
18 | False
19 | pan-down-symbolic
20 |
21 |
22 |
25 |
26 |
27 | False
28 | True
29 | 0
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | [name]
41 | True
42 | True
43 | True
44 |
45 |
46 | False
47 | True
48 | 3
49 |
50 |
51 |
52 |
53 | [score]
54 | True
55 | True
56 | True
57 |
58 |
59 | False
60 | True
61 | 4
62 |
63 |
64 |
65 |
66 | True
67 | True
68 | True
69 |
70 |
71 |
72 | True
73 | False
74 | emblem-favorite-symbolic
75 |
76 |
77 |
78 |
79 | False
80 | True
81 | 5
82 |
83 |
84 |
85 |
86 | [time]
87 | True
88 | True
89 | True
90 |
91 |
92 | False
93 | True
94 | 6
95 |
96 |
97 |
98 |
99 | [subreddit]
100 | True
101 | True
102 | True
103 |
104 |
105 | False
106 | True
107 | 7
108 |
109 |
110 |
111 |
112 | True
113 | True
114 | True
115 |
116 |
117 | True
118 | False
119 | reddit-mail-reply-all-symbolic
120 |
121 |
122 |
123 |
124 | False
125 | True
126 | 8
127 |
128 |
129 |
130 |
131 | True
132 | True
133 | True
134 |
135 |
136 |
137 | True
138 | False
139 | view-refresh-symbolic
140 |
141 |
142 |
143 |
144 | False
145 | True
146 | 9
147 |
148 |
149 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/data/reddit-is-gtk.appdata.xml.in:
--------------------------------------------------------------------------------
1 |
2 |
3 | reddit-is-gtk.desktop
4 | CC0-1.0
5 | GPL-3.0+
6 | <_name>Something For Reddit
7 | <_summary>Waste time with a beautiful GUI
8 |
9 | <_p>
10 | Something for Reddit is a Reddit client for GNOME, built from
11 | the ground up using the Gtk+ 3 framework. It designed with a
12 | usability first focus - supporting both touchscreen and VIM style
13 | keybindings. With this interface, you can view subreddits,
14 | comments, your inbox and user pages. You can sign-into Reddit
15 | accounts with the easy account switcher, allowing you to vote,
16 | write replies and save favorites.
17 |
18 | <_p>
19 | If you hate reading text, here is the same information as above in
20 | dot point format:
21 |
22 |
23 | <_li>Touchscreen tested interface
24 | <_li>VIM style keybindings
25 | <_li>View subreddits, comments and user pages
26 | <_li>Vote on comments and links, write replies
27 | <_li>Integrated WebKit2 browser for vieweing links
28 | <_li>Multi-account support
29 |
30 |
31 |
32 |
33 | https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/master/screenshots/0.2.1-askreddit.png
34 |
35 |
36 | https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/master/screenshots/0.2.1-dankmemes.png
37 |
38 |
39 | https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/master/screenshots/0.2.1-dark.png
40 |
41 |
42 | https://github.com/samdroid-apps/something-for-reddit
43 | https://github.com/samdroid-apps/something-for-reddit/issues
44 | sam@sam.today
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/data/reddit-is-gtk.desktop.in:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | _Name=Something For Reddit
3 | _GenericName=Reddit Browser
4 | _Comment=Waste time with a nice GUI
5 | Exec=reddit-is-gtk
6 | Icon=today.sam.reddit-is-gtk
7 | Terminal=false
8 | Type=Application
9 | Categories=GNOME;GTK;Network;News;
10 | _Keywords=Reddit;GTK;
11 | StartupNotify=true
12 | StartupWMClass=reddit-is-gtk
13 |
--------------------------------------------------------------------------------
/data/reddit-is-gtk.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | style.css.out
5 | style.dark.css.out
6 | app-menu.ui
7 | row-link.ui
8 | user-about.ui
9 | empty-view.ui
10 | row-comment.ui
11 | post-top-bar.ui
12 | submit-window.ui
13 | subreddit-about.ui
14 | webview-toolbar.ui
15 | shortcuts-window.ui
16 | settings-window.ui
17 |
18 |
19 |
--------------------------------------------------------------------------------
/data/row-comment.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | True
7 | False
8 | True
9 |
10 |
11 | 16
12 | True
13 | False
14 | vertical
15 |
16 |
17 | True
18 | False
19 | face-devilish
20 |
21 |
22 | False
23 | True
24 | 0
25 |
26 |
27 |
28 |
29 | True
30 | False
31 | starred-symbolic
32 |
33 |
34 | False
35 | True
36 | 1
37 |
38 |
39 |
40 |
41 | True
42 | False
43 | mail-unread-symbolic
44 |
45 |
46 | False
47 | True
48 | 2
49 |
50 |
51 |
52 |
53 | False
54 | True
55 | 0
56 |
57 |
58 |
59 |
60 | True
61 | False
62 | True
63 |
64 |
65 | 5 hours ago
66 | True
67 | True
68 | True
69 | 0
70 |
73 |
74 |
75 | 0
76 | 0
77 |
78 |
79 |
80 |
81 | Author
82 | True
83 | True
84 | True
85 |
88 |
89 |
90 | 1
91 | 0
92 |
93 |
94 |
95 |
96 | /r/gaming
97 | True
98 | True
99 | True
100 | 1
101 |
104 |
105 |
106 | 2
107 | 0
108 |
109 |
110 |
111 |
112 | True
113 | False
114 | True
115 |
116 |
123 |
124 | False
125 | True
126 | 0
127 |
128 |
129 |
130 |
131 | True
132 | False
133 | 0
134 | mail-unread-symbolic
135 | 3
136 |
137 |
138 | False
139 | True
140 | 1
141 |
142 |
143 |
144 |
145 | True
146 | False
147 | Post title
148 | True
149 | word-char
150 | 0
151 |
152 |
153 |
154 |
155 |
156 | False
157 | True
158 | 2
159 |
160 |
161 |
164 |
165 |
166 | 0
167 | 1
168 | 3
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | False
177 | True
178 | 1
179 |
180 |
181 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/data/settings-window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | False
7 | Settings
8 |
9 |
10 | True
11 | False
12 | 6
13 | 6
14 | 6
15 | 6
16 | vertical
17 | 6
18 |
19 |
20 | True
21 | False
22 |
23 |
24 | True
25 | False
26 | end
27 | center
28 | 10
29 | 10
30 | Theme
31 | 1
32 |
33 |
34 | False
35 | True
36 | 0
37 |
38 |
39 |
40 |
41 | True
42 | False
43 | True
44 |
45 | - Default
46 | - Dark
47 | - Light
48 |
49 |
50 |
51 | False
52 | True
53 | 1
54 |
55 |
56 |
57 |
58 | False
59 | True
60 | 0
61 |
62 |
63 |
64 |
65 | False
66 | center
67 | 10
68 | 10
69 | 10
70 |
71 |
72 | True
73 | False
74 | dialog-warning-symbolic
75 | 6
76 |
77 |
78 | False
79 | True
80 | 0
81 |
82 |
83 |
84 |
85 | True
86 | False
87 | Sam is a <s>horrible programmer</s> doesn't know how
88 | to implement this, so changing the theme will
89 | require a restart
90 | True
91 |
92 |
93 | False
94 | True
95 | 1
96 |
97 |
98 |
99 |
100 | False
101 | True
102 | 1
103 |
104 |
105 |
106 |
107 | True
108 | False
109 |
110 |
111 | True
112 | False
113 | end
114 | center
115 | 10
116 | Default Subreddiit
117 |
118 |
119 | False
120 | True
121 | 0
122 |
123 |
124 |
125 |
126 | True
127 | True
128 | True
129 | /r/defaultsubreddit
130 |
131 |
132 | False
133 | True
134 | 1
135 |
136 |
137 |
138 |
139 | False
140 | True
141 | 2
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/data/style.css:
--------------------------------------------------------------------------------
1 | /* define $dark, based on theme darkness */
2 |
3 | $gnome-gray: if($dark, rgb(51, 57, 59), rgb(232, 232, 231));
4 | $popover-bg: if($dark, rgb(57, 63, 63), rgb(232, 232, 231));
5 | $separator: if($dark, rgb(34, 34, 31), rgb(165, 165, 161));
6 | $blue: if($dark, #215D9C, #4a90d9);
7 | $white: if($dark, black, white);
8 | $orange: if($dark, #ce5c00, #f57900);
9 | $purple: if($dark, #5c3566, #75507b);
10 | $green: if($dark, #4e9a06, #73d216);
11 | $red: if($dark, #a40000, #cc0000);
12 |
13 | /* Some more Tango colors, used for the comments */
14 | $lightskyblue: if($dark, #0a3050, #daeeff);
15 | $lightplum: if($dark, #371740, #fce0ff);
16 | $lightchameleon: if($dark, #2a5703, #e4ffc7);
17 | $lightchocolate: if($dark, #503000, #faf0d7);
18 | $lightorange: if($dark, #8c3700, #fff0d7);
19 |
20 | @mixin button-bg($base-color) {
21 | &, &:backdrop {
22 | color: white;
23 | text-shadow: none;
24 | }
25 | background: linear-gradient($base-color, shade($base-color, 0.8));
26 |
27 | &.flat, &:backdrop {
28 | background: $base-color;
29 | }
30 | &:hover {
31 | background: linear-gradient(lighter($base-color), $base-color);
32 | }
33 | &:active, &:checked {
34 | background: linear-gradient(darker($base-color), $base-color);
35 | }
36 | &:disabled {
37 | background: linear-gradient(darker($base-color), shade($base-color, 0.3));
38 | color: white;
39 | }
40 | }
41 |
42 | .root-comments-bar, .root-comments-label {
43 | margin-left: 12px;
44 | }
45 |
46 | @keyframes angry-comment {
47 | from { box-shadow: inset 0 0 20px $red; }
48 | }
49 |
50 | .comments-view {
51 | background: transparent;
52 |
53 | &.first > row {
54 | padding-left: 12px;
55 | }
56 |
57 | &:not(.first) > row {
58 | margin-left: 12px;
59 | }
60 |
61 | row {
62 | padding: 0;
63 | }
64 |
65 | revealer list {
66 | margin: 0;
67 | }
68 |
69 | row > box > label {
70 | padding-top: 3px;
71 | padding-bottom: 3px;
72 | padding-left: 5px;
73 | border-bottom: none;
74 | }
75 |
76 | row.angry {
77 | animation: 0.5s angry-comment ease-out;
78 | }
79 |
80 | @mixin row($bg) {
81 | & > row {
82 | background: $bg;
83 | transition: background 0.25s linear;
84 | }
85 |
86 | & > row:focus {
87 | @if $dark {
88 | background: darker($bg);
89 | } @else {
90 | background: white;
91 | }
92 | }
93 |
94 | & > row > box > widget > .post-top-bar.linked > button,
95 | & > row > box > button {
96 | @if $dark {
97 | @include button-bg(lighter($bg));
98 | } @else {
99 | @include button-bg(darker($bg));
100 | }
101 | }
102 | }
103 |
104 | &.depth-0 {
105 | @include row($lightskyblue);
106 | }
107 |
108 | &.depth-1 {
109 | @include row($lightplum);
110 | }
111 |
112 | &.depth-2 {
113 | @include row($lightchameleon);
114 | }
115 |
116 | &.depth-3 {
117 | @include row($lightchocolate);
118 | }
119 |
120 | &.depth-4 {
121 | @include row($lightorange);
122 | }
123 | }
124 |
125 | /*
126 | * (Left) list styling
127 | */
128 |
129 | @keyframes pulse {
130 | from { box-shadow: inset 0 0 3px $blue; }
131 | 50% { box-shadow: inset 0 0 6px $blue; }
132 | to { box-shadow: inset 0 0 3px $blue; }
133 | }
134 |
135 | @keyframes angry {
136 | from { box-shadow: inset 0 0 6px $red; }
137 | }
138 |
139 |
140 | .link-row {
141 | border-bottom: 1px solid $separator;
142 |
143 | &.read {
144 | background: $gnome-gray;
145 |
146 | .reply-to-what {
147 | background: $white;
148 | }
149 | }
150 |
151 | &.sticky {
152 | @if $dark {
153 | background: darker(darker($green));
154 | } @else {
155 | background: lighter(lighter($green));
156 | }
157 | }
158 |
159 | &:selected {
160 | background: $blue;
161 |
162 | button.flat:hover, button.flat:checked, button.flat:active {
163 | label {
164 | color: black;
165 | }
166 | }
167 |
168 | .reply-to-what {
169 | background: darker($blue);
170 | }
171 | }
172 |
173 | .reply-to-what {
174 | background: $gnome-gray;
175 | border-radius: 3px;
176 | margin-bottom: 5px;
177 | }
178 |
179 | &:focus {
180 | box-shadow: inset 0 0 6px $blue;
181 | }
182 |
183 | &:selected:focus {
184 | box-shadow: inset 0 0 6px $white;
185 | }
186 |
187 | &.angry {
188 | animation: 0.5s angry ease-out;
189 | }
190 | }
191 |
192 | .about-row {
193 | background: $gnome-gray;
194 | padding: 0 5px 10px 5px;
195 | }
196 |
197 | @mixin label-badge($color) {
198 | box-shadow: 0 0 3px $color,
199 | inset 0 0 3px $color;
200 | background: lighter($color);
201 | color: white;
202 | text-shadow: none;
203 | }
204 |
205 | button {
206 | &.fullscreen {
207 | -gtk-icon-source: -gtk-icontheme('list-add-symbolic');
208 | }
209 |
210 | &.upvoted label {
211 | color: $orange;
212 | }
213 |
214 | &.downvoted label {
215 | color: $purple;
216 | }
217 |
218 | label.gilded {
219 | @include label-badge($orange);
220 | }
221 |
222 | /*
223 | * author distinguished colors,
224 | * made more specific than the comments-view styles
225 | */
226 | label {
227 | border-radius: 3px;
228 | padding-left: 3px;
229 | padding-right: 3px;
230 | }
231 |
232 | label.op {
233 | @include label-badge($blue);
234 | }
235 |
236 | label.moderator {
237 | @include label-badge(darker($green));
238 | }
239 |
240 | label.admin {
241 | @include label-badge(lighter($red));
242 | }
243 |
244 | label.special {
245 | @include label-badge($purple);
246 | }
247 |
248 | label.flair {
249 | border: 1px solid grey;
250 | color: grey;
251 | }
252 |
253 | &.expand image {
254 | transition: 0.2s all linear;
255 | }
256 | &.expand:checked image {
257 | -gtk-icon-transform: rotate(180deg);
258 | }
259 | }
260 |
261 | /* Radio tool buttons are treated separately */
262 | .upvote button:checked {
263 | color: $orange;
264 | }
265 | .downvote button:checked {
266 | color: $purple;
267 | }
268 |
269 | /*
270 | * Misc
271 | */
272 | popover textview {
273 | margin-bottom: 5px;
274 | }
275 |
276 | .error-label {
277 | color: red;
278 | }
279 |
280 | /*
281 | * SubEntry
282 | */
283 |
284 | popover.subentry-palette {
285 | padding-left: 0;
286 | padding-right: 0;
287 | }
288 |
289 | popover.subentry-palette label.header {
290 | padding-left: 6px;
291 | }
292 |
293 | popover button.full-width {
294 | background: $popover-bg;
295 |
296 | margin: 0;
297 | border: 0;
298 | border-radius: 0;
299 | box-shadow: none;
300 |
301 | image {
302 | transition: 0.2s all linear;
303 | }
304 |
305 | &:checked {
306 | background: darker($popover-bg);
307 | border-top: 1px solid black;
308 | color: white;
309 |
310 | image {
311 | -gtk-icon-transform: rotate(-180deg);
312 | }
313 | }
314 | &:hover, &:focus {
315 | background: lighter($popover-bg);
316 | outline: none;
317 |
318 | @if $dark == false {
319 | color: black;
320 | text-shadow: none;
321 | }
322 | }
323 | &:active {
324 | background: $blue;
325 | color: white;
326 | text-shadow: none;
327 | }
328 | }
329 |
330 | popover revealer {
331 | button.full-width {
332 | background: darker($popover-bg);
333 | }
334 |
335 | box {
336 | border-bottom: 1px solid black;
337 | }
338 | }
339 |
340 |
341 | /* MARKDOWN STYLES */
342 | .mdx-block-blockquote {
343 | border-left: 3px solid grey;
344 | padding-left: 6px;
345 | margin-left: 3px;
346 | }
347 | .mdx-block-code {
348 | font-family: monospace;
349 | color: green;
350 | }
351 | .mdx-block > * {
352 | /* Paragraph spacing */
353 | margin-top: 6px;
354 | }
355 | .mdx-block > *:first-child {
356 | margin-top: 0;
357 | }
358 | .mdx-block-ol > *,
359 | .mdx-block-ul > * {
360 | margin-top: 2px;
361 | }
362 |
363 | .mdx-heading {
364 | font-weight: bolder;
365 | }
366 | .mdx-heading-h1 {
367 | font-size: 2em;
368 | margin-top: .67em;
369 | margin-bottom: .67em;
370 | }
371 | .mdx-heading-h2 {
372 | font-size: 1.5em;
373 | margin-top: .75em;
374 | margin-bottom: .75em;
375 | }
376 | .mdx-heading-h3 {
377 | font-size: 1.17em;
378 | margin-top: .83em;
379 | margin-bottom: .83em;
380 | }
381 | .mdx-heading-h5 {
382 | font-size: .83em;
383 | margin-top: 1.5em;
384 | margin-bottom: 1.5em;
385 | }
386 | .mdx-heading-h6 {
387 | font-size: .75em;
388 | margin-top: 1.67em;
389 | margin-bottom: 1.67em;
390 | }
391 |
--------------------------------------------------------------------------------
/data/subreddit-about.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | True
7 | False
8 | document-new-symbolic
9 |
10 |
11 | True
12 | False
13 | vertical
14 |
15 |
16 | True
17 | True
18 | center
19 |
20 |
21 |
22 |
23 |
24 | True
25 | False
26 | AskReddit
27 | center
28 | end
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | False
37 | True
38 | 0
39 |
40 |
41 |
42 |
43 | Submit
44 | True
45 | True
46 | True
47 | center
48 | image1
49 | True
50 |
51 |
52 | False
53 | True
54 | 1
55 |
56 |
57 |
58 |
59 | togglebutton
60 | True
61 | True
62 | True
63 | center
64 |
65 |
66 | False
67 | True
68 | 2
69 |
70 |
71 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/data/today.sam.something-for-reddit.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | '/'
11 | Default subreddit
12 | What to open when you open the app
13 |
14 |
15 | 'default'
16 | Theme to use for app
17 | Theme to use, default just uses the system theme
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/data/user-about.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | True
6 | False
7 | vertical
8 |
9 |
10 | True
11 | False
12 | Person
13 | center
14 | end
15 |
16 |
17 |
18 |
19 |
20 | False
21 | True
22 | 0
23 |
24 |
25 |
26 |
27 | True
28 | False
29 | Loading Karma...
30 |
31 |
32 | False
33 | True
34 | 1
35 |
36 |
37 |
38 |
39 | TODO: PM
40 | True
41 | False
42 | True
43 | True
44 | center
45 |
46 |
47 | False
48 | True
49 | 2
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/data/webview-toolbar.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | True
6 | False
7 |
8 |
9 | True
10 | True
11 | True
12 |
13 |
14 | True
15 | False
16 | go-previous-symbolic
17 |
18 |
19 |
20 |
21 |
22 | False
23 | True
24 | 0
25 |
26 |
27 |
28 |
29 | True
30 | True
31 | True
32 | Open with External Browser
33 |
34 |
35 | True
36 | False
37 | reddit-detach-symbolic
38 |
39 |
40 |
41 |
42 |
43 | False
44 | True
45 | 1
46 |
47 |
48 |
49 |
50 | True
51 | True
52 | True
53 |
54 |
55 | True
56 | False
57 | go-next-symbolic
58 |
59 |
60 |
61 |
62 |
63 | False
64 | True
65 | 2
66 |
67 |
68 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/debian/changelog:
--------------------------------------------------------------------------------
1 | reddit-is-gtk (0.1.0-1) UNRELEASED; urgency=low
2 |
3 | * Initial Debian version
4 |
5 | -- Tobias Graven Tue, 05 Apr 2016 20:27:16 +0800
6 |
--------------------------------------------------------------------------------
/debian/compat:
--------------------------------------------------------------------------------
1 | 9
2 |
--------------------------------------------------------------------------------
/debian/control:
--------------------------------------------------------------------------------
1 | Source: reddit-is-gtk
2 | Maintainer: samdroid-apps
3 | Section: web
4 | Priority: optional
5 | Standards-Version: 3.9.5
6 | Build-Depends: debhelper (>= 9), dh-autoreconf, intltool (>= 0.28), gettext, libgtk-3-dev (>= 3.10), libglib2.0-dev (>= 2.28), libsoup2.4-dev, python3-arrow, python3-markdown, ruby-sass
7 |
8 | Package: reddit-is-gtk
9 | Architecture: any
10 | Depends: ${shlibs:Depends}, ${misc:Depends}, libc6, gir1.2-webkit2-3.0, libgtk-3-0 (>= 3.10), libglib2.0-0 (>= 2.28), libsoup2.4-1, python3-arrow, python3-markdown
11 | Description: Reddit is GTK
12 | A simple Reddit client for GNOME, built for touch and mouse.
13 |
--------------------------------------------------------------------------------
/debian/copyright:
--------------------------------------------------------------------------------
1 | reddit-is-gtk - A Reddit client for GNOME
2 | Copyright (C) 2016 Sam "samdroid-apps"
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with this program. If not, see , or
16 | /usr/share/common-licenses if you are using a Debian system.
17 |
--------------------------------------------------------------------------------
/debian/rules:
--------------------------------------------------------------------------------
1 | #!/usr/bin/make -f
2 | %:
3 | dh $@ --with autoreconf
4 |
5 |
--------------------------------------------------------------------------------
/debian/source/format:
--------------------------------------------------------------------------------
1 | 3.0 (quilt)
2 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | with import {};
2 |
3 | let
4 | py = pkgs.python3;
5 | app = callPackage ./app.nix {};
6 | in
7 | stdenv.mkDerivation rec {
8 | name = "env";
9 |
10 | buildInputs = [
11 | app
12 | ];
13 |
14 | shellHook = ''
15 | export FISH_P1="(sfr) "
16 | export FISH_P1_COLOR="green"
17 | '';
18 | }
19 |
--------------------------------------------------------------------------------
/dev-shell.nix:
--------------------------------------------------------------------------------
1 | with import {};
2 |
3 | let
4 | py = pkgs.python3;
5 | app = callPackage ./app.nix {};
6 |
7 | helpMessage = ''
8 | To setup the build environment, you will need to run autogen:
9 |
10 | ./autogen.sh --prefix=$PREFIX
11 |
12 | To build, you can use the following commands:
13 |
14 | make; and make install; and reddit-is-gtk
15 |
16 | The environment is configured so that the builds will be installed
17 | into the `__build_prefix` directory. Root is not required.
18 | '';
19 | in
20 | stdenv.mkDerivation rec {
21 | name = "env";
22 |
23 | buildInputs = app.nativeBuildInputs ++ app.buildInputs;
24 |
25 | shellHook = ''
26 | export FISH_P1="(sfr-dev) "
27 | export FISH_P1_COLOR="magenta"
28 |
29 | export PREFIX=$(pwd)/__build_prefix
30 |
31 | export XDG_DATA_DIRS=$PREFIX/share:${gnome3.gnome_themes_standard}/share:$XDG_DATA_DIRS:$XDG_ICON_DIRS:$GSETTINGS_SCHEMAS_PATH
32 | export GI_TYPELIB_PATH=${app.extraTypelibPath}:$GI_TYPELIB_PATH
33 | export LD_LIBRARY_PATH=${app.extraLibPath}:$LD_LIBRARY_PATH
34 | export PYTHONPATH=.:$PREFIX/lib/python3.6/site-packages:$PYTHONPATH
35 | export PATH=$PREFIX/bin:$PATH
36 |
37 | echo '${helpMessage}'
38 | '';
39 | }
40 |
--------------------------------------------------------------------------------
/fedora/fedora.spec:
--------------------------------------------------------------------------------
1 | %global gobject_introspection_version 1.35.9
2 | %global gtk3_version 3.13.2
3 | %global soup_version 2.4
4 |
5 | Name: something-for-reddit
6 | Summary: Browse Reddit from GNOME
7 | Version: 0.2
8 | Release: 1%{?dist}
9 | BuildArch: noarch
10 |
11 | License: GPLv3+
12 | URL: https://github.com/samdroid-apps/something-for-reddit
13 | Source0: https://download.gnome.org/sources/%{name}/3.20/%{name}-%{version}.tar.xz
14 |
15 | BuildRequires: /usr/bin/appstream-util
16 | BuildRequires: desktop-file-utils
17 | BuildRequires: intltool
18 | BuildRequires: itstool
19 | BuildRequires: pkgconfig(gio-2.0)
20 | BuildRequires: pkgconfig(gobject-introspection-1.0) >= %{gobject_introspection_version}
21 | BuildRequires: pkgconfig(gtk+-3.0) >= %{gtk3_version}
22 | BuildRequires: pkgconfig(libsoup-2.4) >= %{soup_version}
23 | BuildRequires: python3-rpm-macros
24 | BuildRequires: python3-arrow
25 | BuildRequires: python3-markdown
26 | BuildRequires: sassc
27 | BuildRequires: gnome-common
28 |
29 | Requires: libsoup
30 | Requires: gobject-introspection >= %{gobject_introspection_version}
31 | Requires: gtk3 >= %{gtk3_version}
32 | Requires: pango
33 | Requires: python3-gobject
34 | Requires: python3-arrow
35 | Requires: python3-markdown
36 |
37 | %description
38 | This is a Reddit client, built with Gtk+ and optimized for GNOME.
39 |
40 |
41 | %prep
42 | %autosetup -p1
43 |
44 |
45 | %build
46 | %configure --disable-silent-rules
47 | libtoolize
48 | intltoolize --force
49 | aclocal
50 | #. gnome-autogen.sh
51 | make %{?_smp_mflags}
52 |
53 |
54 | %install
55 | %make_install
56 |
57 |
58 | %check
59 | appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/appdata/reddit-is-gtk.appdata.xml
60 | desktop-file-validate %{buildroot}/%{_datadir}/applications/reddit-is-gtk.desktop
61 |
62 |
63 |
64 | %files
65 | %doc NEWS
66 | %license COPYING
67 | %{_bindir}/reddit-is-gtk
68 | %{_datadir}/%{name}
69 | %{_datadir}/icons
70 | %{_datadir}/something-for-reddit
71 | %{_datadir}/appdata/reddit-is-gtk.appdata.xml
72 | %{_datadir}/glib-2.0/schemas/today.sam.something-for-reddit.gschema.xml
73 | %{_datadir}/applications/reddit-is-gtk.desktop
74 | %{python3_sitelib}/redditisgtk
75 |
--------------------------------------------------------------------------------
/po/.intltool-merge-cache:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/po/.intltool-merge-cache
--------------------------------------------------------------------------------
/po/POTFILES:
--------------------------------------------------------------------------------
1 | ../data/app-menu.ui \
2 | ../data/post-top-bar.ui \
3 | ../data/reddit-is-gtk.appdata.xml.in \
4 | ../data/reddit-is-gtk.desktop.in \
5 | ../data/row-link.ui \
6 | ../data/submit-window.ui \
7 | ../data/subreddit-about.ui \
8 | ../data/user-about.ui \
9 | ../data/webview-toolbar.ui
10 |
--------------------------------------------------------------------------------
/po/POTFILES.in:
--------------------------------------------------------------------------------
1 | data/app-menu.ui
2 | data/post-top-bar.ui
3 | data/reddit-is-gtk.appdata.xml.in
4 | data/reddit-is-gtk.desktop.in
5 | data/row-link.ui
6 | data/submit-window.ui
7 | data/subreddit-about.ui
8 | data/user-about.ui
9 | data/webview-toolbar.ui
10 |
11 |
--------------------------------------------------------------------------------
/po/POTFILES.skip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/po/POTFILES.skip
--------------------------------------------------------------------------------
/publish-flatpak.md:
--------------------------------------------------------------------------------
1 | Run this command to build the app (remembering to change the subject):
2 |
3 | ```sh
4 | flatpak-builder build-dir \
5 | today.sam.reddit-is-gtk.json \
6 | --force-clean \
7 | --gpg-sign=34E268B2FA2F8B13 \
8 | --repo=/home/sam/sam.today-flatpak-repo \
9 | --default-branch=stable \
10 | --subject="Testing build of Something for Reddit"
11 | ```
12 |
13 | Then inside the repo directory, run this:
14 |
15 | ```sh
16 | flatpak build-update-repo \
17 | --prune \
18 | --prune-depth=20 \
19 | --gpg-sign=34E268B2FA2F8B13 \
20 | .
21 | rsync -rapPhz --delete . root@nix1.sam.today:/srv/flatpak.dl.sam.today/
22 | ```
23 |
24 |
25 |
--------------------------------------------------------------------------------
/reddit-is-gtk.in:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import sys
3 |
4 | # Make sure that where we installed it to, python for look
5 | # important for eg. /usr/local
6 | sys.path.insert(1, '@pythondir@')
7 |
8 | from gi.repository import Gio
9 | r = Gio.resource_load('@pkgdatadir@/reddit-is-gtk.gresource')
10 | Gio.Resource._register(r)
11 |
12 | import redditisgtk.main
13 | redditisgtk.main.run()
14 |
--------------------------------------------------------------------------------
/redditisgtk/Makefile.am:
--------------------------------------------------------------------------------
1 | appdir = $(pythondir)/redditisgtk
2 |
3 | app_PYTHON = \
4 | Makefile \
5 | Makefile.am \
6 | Makefile.in \
7 | __init__.py \
8 | aboutrow.py \
9 | api.py \
10 | buttons.py \
11 | comments.py \
12 | emptyview.py \
13 | gtkutil.py \
14 | identity.py \
15 | identitybutton.py \
16 | main.py \
17 | mediapreview.py \
18 | newmarkdown.py \
19 | palettebutton.py \
20 | posttopbar.py \
21 | readcontroller.py \
22 | settings.py \
23 | subentry.py \
24 | sublist.py \
25 | sublistrows.py \
26 | submit.py \
27 | webviews.py
28 |
--------------------------------------------------------------------------------
/redditisgtk/__init__.py:
--------------------------------------------------------------------------------
1 | import gi
2 |
3 | gi.require_version('Gtk', '3.0')
4 | gi.require_version('Soup', '2.4')
5 | gi.require_version('WebKit2', '4.0')
6 |
--------------------------------------------------------------------------------
/redditisgtk/aboutrow.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | from gi.repository import Gtk
19 |
20 | from redditisgtk import newmarkdown
21 | from redditisgtk.buttons import SubscribeButtonBehaviour
22 | from redditisgtk import submit
23 | from redditisgtk.api import RedditAPI
24 |
25 |
26 | class AboutRow(Gtk.ListBoxRow):
27 | '''
28 | Abstract base class for AboutRows
29 | '''
30 |
31 | def __init__(self, **kwargs):
32 | Gtk.ListBoxRow.__init__(self, **kwargs)
33 |
34 |
35 | class _SubredditAboutRow(AboutRow):
36 |
37 | def __init__(self, api: RedditAPI, subreddit_name: str):
38 | super().__init__(selectable=False)
39 |
40 | self._subreddit_name = subreddit_name
41 | self._api = api
42 | self._loaded = False
43 |
44 | self._builder = Gtk.Builder.new_from_resource(
45 | '/today/sam/reddit-is-gtk/subreddit-about.ui')
46 | self._g = self._builder.get_object
47 |
48 | self.add(self._g('box'))
49 | self._g('subreddit').props.label = self._subreddit_name
50 | self._sbb = SubscribeButtonBehaviour(
51 | self._api, self._g('subscribe'), self._subreddit_name)
52 | self._g('submit').connect('clicked', self.__submit_clicked_cb)
53 | self._g('expander').connect(
54 | 'notify::expanded', self.__notify_expanded_cb)
55 |
56 | def __submit_clicked_cb(self, button):
57 | w = submit.SubmitWindow(self._api, sub=self._subreddit_name)
58 | w.show()
59 |
60 | def __notify_expanded_cb(self, expander, pspec):
61 | if not self._loaded:
62 | self._api.get_subreddit_info(
63 | self._subreddit_name, self.__got_info_cb)
64 | self._loaded = True
65 |
66 | def __got_info_cb(self, data):
67 | expander = self._g('expander')
68 | child = expander.get_child()
69 | if child is not None:
70 | expander.remove(ch)
71 | ch.destroy()
72 |
73 | markdown = data['data']['description']
74 | expander.add(newmarkdown.make_markdown_widget(markdown))
75 |
76 |
77 | class _UserAboutRow(AboutRow):
78 |
79 | def __init__(self, api: RedditAPI, name: str):
80 | super().__init__(selectable=False)
81 |
82 | self._name = name
83 |
84 | self._builder = Gtk.Builder.new_from_resource(
85 | '/today/sam/reddit-is-gtk/user-about.ui')
86 | self._g = self._builder.get_object
87 |
88 | self.add(self._g('box'))
89 | self._g('name').props.label = self._name
90 |
91 | api.get_user_info(
92 | self._name, self.__got_info_cb)
93 |
94 | def __got_info_cb(self, data):
95 | data = data['data']
96 | self._g('karma').props.label = \
97 | '{link_karma}l / {comment_karma}c'.format(**data)
98 |
99 |
100 | def get_about_row(api: RedditAPI, sub: str):
101 | # Disregard leading slash
102 | url_parts = sub.strip('/').split('/')
103 |
104 | # Show if it is like /r/sub
105 | if len(url_parts) >= 2 and url_parts[0] == 'r' and url_parts[1] != 'all':
106 | return _SubredditAboutRow(api, url_parts[1])
107 |
108 | # Eg. /user/name(/*)
109 | if len(url_parts) >= 2 and url_parts[0] in ('user', 'u'):
110 | return _UserAboutRow(api, url_parts[1])
111 |
112 | return None
113 |
--------------------------------------------------------------------------------
/redditisgtk/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from pathlib import Path
4 | from tempfile import TemporaryDirectory
5 |
6 | from pytest import fixture
7 |
8 |
9 | @fixture
10 | def datadir() -> Path:
11 | '''
12 | Fixture that gives the path of the data directory
13 | '''
14 | return Path(__file__).absolute().parent / 'tests-data'
15 |
16 |
17 | @fixture
18 | def json_loader():
19 | '''
20 | Fixture that injects a json loading function
21 | '''
22 | def inner(name: str) -> dict:
23 | path = datadir() / (name + '.json')
24 | with open(path) as f:
25 | return json.load(f)
26 | return inner
27 |
28 |
29 | @fixture
30 | def tempdir() -> Path:
31 | '''
32 | Fixture that gives you the path of a new temporary directory
33 | '''
34 | with TemporaryDirectory() as dirname:
35 | yield Path(dirname).absolute()
36 |
37 |
38 | def assert_matches_snapshot(name: str, data: dict):
39 | '''
40 | Checks that the data matches the data stored in the snapshot.
41 |
42 | If the snapshot does not exist, it creates a snapshot with the passed data
43 | '''
44 | snap_dir = Path(__file__).absolute().parent / 'tests-data' / 'snapshots'
45 | path = snap_dir / (name + '.json')
46 |
47 | if path.exists():
48 | with open(path) as f:
49 | expected = json.load(f)
50 | assert data == expected
51 | else: # pragma: no cover
52 | if 'CI' in os.environ:
53 | raise Exception('Snapshot not found in CI environment')
54 |
55 | with open(path, 'w') as f:
56 | json.dump(data, f, indent=2, sort_keys=True)
57 |
--------------------------------------------------------------------------------
/redditisgtk/emptyview.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | from gi.repository import GObject
19 | from gi.repository import Gtk
20 |
21 | class EmptyView(Gtk.Bin):
22 |
23 | action = GObject.Signal('action')
24 |
25 | def __init__(self, title: str, action: str = None):
26 | Gtk.Bin.__init__(self)
27 |
28 | builder = Gtk.Builder.new_from_resource(
29 | '/today/sam/reddit-is-gtk/empty-view.ui')
30 | self._g = builder.get_object
31 | self.add(self._g('box'))
32 | self.get_child().show()
33 |
34 | self._g('title').props.label = title
35 |
36 | if action is not None:
37 | self._g('action').props.label = action
38 | self._g('action').props.visible = True
39 | self._g('action').connect('clicked', self.__action_clicked_cb)
40 |
41 | def __action_clicked_cb(self, button):
42 | self.action.emit()
43 |
--------------------------------------------------------------------------------
/redditisgtk/gtktestutil.py:
--------------------------------------------------------------------------------
1 | import time
2 | import typing
3 | import functools
4 | from unittest.mock import MagicMock
5 |
6 | from gi.repository import Gtk
7 | from gi.repository import Gdk
8 | from gi.repository import GLib
9 | from gi.repository import Gio
10 |
11 |
12 | def with_test_mainloop(func):
13 | @functools.wraps(func)
14 | def wrapper(*args, **kwargs):
15 | # It would probably be slightly better to find this from PATH
16 | r = Gio.resource_load('./__build_prefix/share/something-for-reddit'
17 | '/reddit-is-gtk.gresource')
18 | Gio.Resource._register(r)
19 |
20 | class MyApplication(Gtk.Application):
21 | def __init__(self):
22 | Gtk.Application.__init__(self, application_id='today.sam.reddit-is-gtk')
23 | self.exception = None
24 | self.retval = None
25 |
26 | def do_activate(self):
27 | try:
28 | self.retval = func(*args, **kwargs)
29 | except Exception as e:
30 | self.exception = e
31 | app.quit()
32 |
33 | app = MyApplication()
34 | app.run()
35 | Gio.Resource._unregister(r)
36 | if app.exception is not None:
37 | raise app.exception
38 | return app.retval
39 |
40 | return wrapper
41 |
42 |
43 | def _iter_all_widgets(root: Gtk.Widget):
44 | yield root
45 | if isinstance(root, Gtk.Container):
46 | for child in root.get_children():
47 | yield from _iter_all_widgets(child)
48 |
49 |
50 | def get_focused(root: Gtk.Widget) -> Gtk.Widget:
51 | for widget in _iter_all_widgets(root):
52 | if widget.is_focus():
53 | return widget
54 |
55 |
56 | def get_label_for_widget(widget: Gtk.Widget):
57 | my_label = None
58 | if hasattr(widget, 'get_label'):
59 | my_label = widget.get_label()
60 |
61 | # Mainly for the stackswitcher radio buttons
62 | if not my_label and hasattr(widget, 'get_child'):
63 | child = widget.get_child()
64 | if hasattr(child, 'get_label'):
65 | my_label = child.get_label()
66 |
67 | if not my_label and hasattr(widget, 'get_text'):
68 | my_label = widget.get_text()
69 |
70 | return my_label
71 |
72 |
73 | def debug_print_widgets(root: Gtk.Widget): # pragma: no cover
74 | for widget in _iter_all_widgets(root):
75 | print(widget, get_label_for_widget(widget))
76 |
77 |
78 | def snapshot_widget(root: Gtk.Widget) -> dict:
79 | style = root.get_style_context()
80 | snap = {
81 | 'type': type(root).__name__,
82 | 'label': get_label_for_widget(root),
83 | 'classes': style.list_classes()}
84 |
85 | if isinstance(root, Gtk.Container):
86 | snap['children'] = list(map(snapshot_widget, root.get_children()))
87 |
88 | return snap
89 |
90 |
91 | def find_widget(root: Gtk.Widget,
92 | kind: typing.Type[Gtk.Widget] = None,
93 | label: str = None,
94 | placeholder: str = None,
95 | many: bool = False):
96 | found = []
97 | for widget in _iter_all_widgets(root):
98 | if kind is not None and not isinstance(widget, kind):
99 | continue
100 |
101 | if label is not None and get_label_for_widget(widget) != label:
102 | continue
103 |
104 | my_placeholder = None
105 | if hasattr(widget, 'get_placeholder_text'):
106 | my_placeholder = widget.get_placeholder_text()
107 | if placeholder and my_placeholder != placeholder:
108 | continue
109 |
110 | found.append(widget)
111 |
112 | if many:
113 | return found
114 | else:
115 | if len(found) == 1:
116 | return found[0]
117 | else:
118 | debug_print_widgets(root)
119 | assert len(found) == 1
120 |
121 |
122 | def wait_for(cond: typing.Callable[[], bool], timeout: float= 5):
123 | start_time = time.time()
124 | while True:
125 | if cond():
126 | return
127 |
128 | if time.time() - start_time > timeout:
129 | raise AssertionError('Timeout expired')
130 |
131 | Gtk.main_iteration_do(False)
132 |
133 |
134 | def fake_event(keyval, event_type=Gdk.EventType.KEY_PRESS, ctrl=False):
135 | if isinstance(keyval, str):
136 | keyval = ord(keyval)
137 |
138 | ev = MagicMock()
139 | ev.type = event_type
140 | ev.keyval = keyval
141 | if ctrl:
142 | ev.state = Gdk.ModifierType.CONTROL_MASK
143 | else:
144 | ev.state = 0
145 | return ev
146 |
--------------------------------------------------------------------------------
/redditisgtk/gtkutil.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | from gi.repository import Gtk
19 | from gi.repository import Gdk
20 |
21 |
22 | def process_shortcuts(shortcuts, event: Gdk.Event):
23 | '''
24 | Shortcuts is a dict of:
25 | accelerator string: (self._function, [arguments])
26 |
27 | Accelerator is passed to Gtk.accelerator_parse
28 | Event is the GdkEvent
29 | '''
30 | if event.type != Gdk.EventType.KEY_PRESS:
31 | return
32 | for accel_string, value in shortcuts.items():
33 | key, mods = Gtk.accelerator_parse(accel_string)
34 | emods = event.state & (Gdk.ModifierType.CONTROL_MASK |
35 | Gdk.ModifierType.SHIFT_MASK)
36 |
37 | if event.keyval == key and (emods & mods or mods == emods == 0):
38 | func, args = value
39 | try:
40 | func(*args)
41 | except Exception as e:
42 | raise
43 | return False
44 | return True
45 |
--------------------------------------------------------------------------------
/redditisgtk/identity.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | from gi.repository import Gtk
19 | from gi.repository import GLib
20 | from gi.repository import Soup
21 | from gi.repository import WebKit2
22 | from gi.repository import GObject
23 |
24 | import os
25 | import json
26 | import urllib.parse
27 | from uuid import uuid4
28 |
29 | from redditisgtk import api
30 | from redditisgtk.readcontroller import get_data_file_path
31 |
32 |
33 | class IdentityController(GObject.GObject):
34 |
35 | token_changed = GObject.Signal('token-changed', arg_types=[])
36 |
37 | def __init__(self, session: Soup.Session, data_path: str = None):
38 | super().__init__()
39 |
40 | self._data_path = data_path
41 | if self._data_path is None:
42 | self._data_path = get_data_file_path('identity')
43 |
44 | self._session = session
45 | self._active = None
46 | self._tokens = {}
47 | self._anon = api.AnonymousTokenManager()
48 |
49 | self.load()
50 |
51 | @property
52 | def active_token(self) -> api.TokenManager:
53 | if self._active is None:
54 | return self._anon
55 | else:
56 | return self._tokens[self._active]
57 |
58 | def load(self):
59 | if os.path.isfile(self._data_path):
60 | with open(self._data_path) as f:
61 | j = json.load(f)
62 |
63 | self._tokens = {}
64 | for id_, data in j['tokens'].items():
65 | token = api.OAuthTokenManager(
66 | self._session,
67 | token=data,
68 | )
69 | token.value_changed.connect(self._token_value_changed_cb)
70 | self._tokens[id_] = token
71 | self._active = j['active']
72 |
73 | self.token_changed.emit()
74 |
75 | def _token_value_changed_cb(self, token):
76 | if token == self.active_token:
77 | self.token_changed.emit()
78 | self.save()
79 |
80 | def save(self):
81 | with open(self._data_path, 'w') as f:
82 | json.dump({
83 | 'tokens': \
84 | {k: v.serialize() for k, v in self._tokens.items()},
85 | 'active': self._active,
86 | }, f)
87 |
88 | @property
89 | def all_tokens(self):
90 | yield from self._tokens.items()
91 |
92 | def switch_account(self, id):
93 | if self._active == id:
94 | return
95 |
96 | self._active = id
97 | if self._active is not None:
98 | self._tokens[self._active].refresh(lambda: self.token_changed.emit())
99 | else:
100 | self.token_changed.emit()
101 | self.save()
102 |
103 | def remove_account(self, id):
104 | if self._active == id:
105 | self._active = None
106 | del self._tokens[id]
107 | self.token_changed.emit()
108 | self.save()
109 |
110 | def sign_in_got_code(self, code, callback):
111 | id = str(uuid4())
112 |
113 | def done_cb():
114 | self._active = id
115 | self.token_changed.emit()
116 | self.save()
117 | callback()
118 |
119 | self._tokens[id] = api.OAuthTokenManager(
120 | self._session,
121 | code=code,
122 | ready_callback=done_cb)
123 | self._tokens[id].value_changed.connect(self._token_value_changed_cb)
124 |
--------------------------------------------------------------------------------
/redditisgtk/identitybutton.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | import urllib.parse
19 | from uuid import uuid4
20 |
21 | from gi.repository import Gtk
22 | from gi.repository import WebKit2
23 |
24 | from redditisgtk.identity import IdentityController
25 |
26 |
27 | class IdentityButton(Gtk.MenuButton):
28 |
29 | def __init__(self, ic: IdentityController):
30 | Gtk.MenuButton.__init__(self, popover=_IdentityPopover(ic))
31 | self.__token_cb(ic)
32 | ic.token_changed.connect(self.__token_cb)
33 |
34 | def __token_cb(self, ctrl):
35 | token = ctrl.active_token
36 | self.props.label = token.user_name
37 |
38 |
39 | class _IdentityPopover(Gtk.Popover):
40 |
41 | def __init__(self, ic: IdentityController):
42 | Gtk.Popover.__init__(self)
43 | self.__token_cb(ic)
44 | ic.token_changed.connect(self.__token_cb)
45 | self._ic = ic
46 |
47 | def __token_cb(self, ctrl):
48 | if self.get_child() is not None:
49 | c = self.get_child()
50 | self.remove(c)
51 | c.destroy()
52 |
53 | listbox = Gtk.ListBox()
54 | self.add(listbox)
55 | listbox.show()
56 |
57 | anon = _AccountRow(ctrl, None, 'Anonymous', removeable=False)
58 | listbox.add(anon)
59 | anon.show()
60 | if ctrl.active_token.is_anonymous:
61 | listbox.select_row(anon)
62 |
63 | for id, token in ctrl.all_tokens:
64 | row = _AccountRow(ctrl, id, token.user_name)
65 | listbox.add(row)
66 | row.show()
67 | if token == ctrl.active_token:
68 | listbox.select_row(row)
69 |
70 | add = Gtk.Button(label='Add new account',
71 | always_show_image=True)
72 | add.connect('clicked', self.__add_clicked_cb)
73 | add.add(Gtk.Image.new_from_icon_name(
74 | 'list-add-symbolic', Gtk.IconSize.MENU))
75 | add.get_style_context().add_class('flat')
76 | listbox.add(add)
77 | add.show()
78 |
79 | listbox.connect('row-selected', self.__row_selected_cb)
80 |
81 | def __add_clicked_cb(self, button):
82 | w = SignInWindow(self._ic)
83 | w.show()
84 |
85 | def __row_selected_cb(self, listbox, row):
86 | if row is not None:
87 | self._ic.switch_account(row.id)
88 | return True
89 |
90 |
91 | class _AccountRow(Gtk.ListBoxRow):
92 |
93 | def __init__(self, ic, id, name, removeable=True):
94 | Gtk.ListBoxRow.__init__(self)
95 | self._ic = ic
96 | self.id = id
97 | self.name = name
98 |
99 | box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
100 | self.add(box)
101 | box.add(Gtk.Label(label=name))
102 |
103 | if removeable:
104 | remove = Gtk.Button()
105 | remove.connect('clicked', self.__remove_cb)
106 | remove.get_style_context().add_class('flat')
107 | remove.add(Gtk.Image.new_from_icon_name(
108 | 'edit-delete-symbolic', Gtk.IconSize.MENU))
109 | box.pack_end(remove, False, False, 0)
110 | self.show_all()
111 |
112 | def __remove_cb(self, button):
113 | self._ic.remove_account(self.id)
114 |
115 |
116 | class SignInWindow(Gtk.Window):
117 |
118 | def __init__(self, ic: IdentityController):
119 | Gtk.Window.__init__(self)
120 | self.set_default_size(400, 400)
121 | self.set_title('Sign into Reddit')
122 | self._state = str(uuid4())
123 | self._ic = ic
124 |
125 | ctx = WebKit2.WebContext.get_default()
126 | ctx.register_uri_scheme('redditgtk', self.__uri_scheme_cb)
127 |
128 | self._web = WebKit2.WebView()
129 | self._web.load_uri(
130 | 'https://www.reddit.com/api/v1/authorize.compact?{end}'.format(
131 | end=urllib.parse.urlencode(dict(
132 | redirect_uri='redditgtk://done',
133 | state=self._state,
134 | client_id='WCN3jqoJ1-0r0Q',
135 | response_type='code',
136 | duration='permanent',
137 | scope=('edit history identity mysubreddits privatemessages'
138 | ' submit subscribe vote read save')))
139 | ))
140 | self._web.connect('notify::uri', self.__notify_uri_cb)
141 | self.add(self._web)
142 | self.show_all()
143 |
144 | def __uri_scheme_cb(self, request):
145 | # WebKit now has a security measure that doesn't allow a http(s) site
146 | # to redirect to a non-http(s) uri scheme. So this only actually gets
147 | # called for the old versions of webkit
148 | pass
149 |
150 | def __notify_uri_cb(self, webview, pspec):
151 | uri = urllib.parse.urlparse(webview.props.uri)
152 |
153 | if uri.scheme == 'redditgtk':
154 | self._show_label()
155 |
156 | d = urllib.parse.parse_qs(uri.query)
157 | if d['state'][0] != self._state:
158 | self._label.set_markup(
159 | 'Reddit did not return the same state in OAuth flow')
160 | self._close.show()
161 | return
162 |
163 | if d.get('code'):
164 | self._ic.sign_in_got_code(d['code'][0], self.__done_cb)
165 | self._label.set_markup('Going down the OAuth flow')
166 | self._spinner.show()
167 | else:
168 | self._label.set_markup(
169 | 'Reddit OAuth Error {}'.format(d['error']))
170 | self._close.show()
171 |
172 | def __done_cb(self):
173 | self.destroy()
174 |
175 | def _show_label(self):
176 | self._web.hide()
177 | self.remove(self._web)
178 | self._web.destroy()
179 |
180 | box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
181 | self.add(box)
182 | box.show()
183 | self._label = Gtk.Label()
184 | box.add(self._label)
185 | self._label.show()
186 | self._spinner = Gtk.Spinner()
187 | box.add(self._spinner)
188 |
189 | self._close = Gtk.Button(label='Close')
190 | self._close.get_style_context().add_class('primary-action')
191 | self._close.connect('clicked', self.destroy)
192 | box.add(self._close)
193 |
--------------------------------------------------------------------------------
/redditisgtk/mediapreview.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | from gi.repository import Gtk
19 | from gi.repository import Gdk
20 | from gi.repository import GdkPixbuf
21 |
22 | import subprocess
23 | from tempfile import mkstemp
24 | import urllib.parse
25 | from html.parser import HTMLParser
26 |
27 | from redditisgtk.api import RedditAPI
28 | from redditisgtk.webviews import FullscreenableWebview
29 |
30 |
31 | def _unescape(s):
32 | return urllib.parse.unquote(
33 | s.replace('<', '<').replace('>', '>')
34 | .replace('&', '&'))
35 |
36 |
37 | class _IframeSrcGetter(HTMLParser):
38 | src = None
39 |
40 | def handle_starttag(self, tag, attrs):
41 | if tag == 'iframe':
42 | for k, v in attrs:
43 | if k == 'src':
44 | self.src = v
45 |
46 |
47 | def get_preview_palette(api: RedditAPI, listing, **kwargs):
48 | if 'content' in listing.get('media_embed', {}):
49 | embed = listing['media_embed']
50 | parser = _IframeSrcGetter()
51 | parser.feed(_unescape(embed['content']))
52 | return WebViewPopover(
53 | uri=parser.src,
54 | width=embed['width'], height=embed['height'],
55 | **kwargs)
56 | elif len(listing.get('preview', {}).get('images', [])):
57 | uri = listing['preview']['images'][0]['source']['url']
58 | return _ImagePreviewPalette(
59 | api, uri, **kwargs)
60 | return None
61 |
62 |
63 | class WebViewPopover(Gtk.Popover):
64 |
65 | def __init__(self, uri, width, height, **kwargs):
66 | if uri.startswith('//'):
67 | uri = 'https:' + uri
68 |
69 | Gtk.Popover.__init__(self, **kwargs)
70 | self._wv = FullscreenableWebview()
71 | self._wv.load_uri(uri)
72 | self._wv.set_size_request(width, height)
73 | self.add(self._wv)
74 | self._wv.show()
75 |
76 |
77 | class _ImagePreviewPalette(Gtk.Popover):
78 | # TODO: Scrolling and scaling
79 |
80 | def __init__(self, api: RedditAPI, uri, **kwargs):
81 | Gtk.Popover.__init__(self, **kwargs)
82 | overlay = Gtk.Overlay()
83 | self.add(overlay)
84 | overlay.show()
85 |
86 | win_w, win_h = self.get_toplevel().get_size()
87 | max_w = min(Gdk.Screen.width(), win_w / 2)
88 | max_h = min(Gdk.Screen.height(), win_h / 2)
89 |
90 | self._image = _RemoteImage(api, uri, max_w, max_h)
91 | overlay.add(self._image)
92 | self._image.show()
93 |
94 | self._eog = Gtk.Button(halign=Gtk.Align.END,
95 | valign=Gtk.Align.START,
96 | label='EoG')
97 | self._eog.connect('clicked', self.__eog_clicked_cb)
98 | overlay.add_overlay(self._eog)
99 | self._eog.show()
100 |
101 | def __eog_clicked_cb(self, button):
102 | fd, path = mkstemp()
103 | self._image.save_to(path)
104 | subprocess.call(['eog', '--fullscreen', path])
105 |
106 |
107 | class _RemoteImage(Gtk.Bin):
108 | # TODO: Reload on error
109 |
110 | def __init__(self, api: RedditAPI, uri: str, max_w: float, max_h: float):
111 | # TODO: this really shouldn't need a reddit api. Maybe just a soup
112 | # session? This feels like a bad DI pattern right now
113 | Gtk.Bin.__init__(self)
114 | self._max_w = max_w
115 | self._max_h = max_h
116 |
117 | self._spinner = Gtk.Spinner()
118 | self.add(self._spinner)
119 | self._spinner.show()
120 |
121 | self._image = Gtk.Image()
122 | self.add(self._image)
123 | api.download_thumb(uri, self.__message_done_cb)
124 |
125 | def __message_done_cb(self, pixbuf):
126 | old_sr = self.get_size_request()
127 |
128 | # GtkImage can not scale images internally, so we need to
129 | # pre-process that
130 | scale = min(
131 | self._max_w / pixbuf.props.width,
132 | self._max_h / pixbuf.props.height)
133 | if scale < 1:
134 | new_w = pixbuf.props.width * scale
135 | new_h = pixbuf.props.height * scale
136 | pixbuf = pixbuf.scale_simple(
137 | new_w, new_h, GdkPixbuf.InterpType.BILINEAR)
138 |
139 | self._image.props.pixbuf = pixbuf
140 | self.remove(self.get_child())
141 | self.add(self._image)
142 | self._image.show()
143 | self._image.set_size_request(*old_sr)
144 |
145 | def save_to(self, path):
146 | self._image.props.pixbuf.savev(path, 'png', [], [])
147 |
--------------------------------------------------------------------------------
/redditisgtk/newmarkdown.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from xml.etree import ElementTree
3 | from xml.etree.ElementTree import Element
4 | import html
5 |
6 | import markdown
7 | from markdown.extensions import Extension
8 | from markdown.inlinepatterns import Pattern, SimpleTagPattern
9 | from markdown.treeprocessors import Treeprocessor
10 |
11 | from gi.repository import Gtk
12 | from gi.repository import Gdk
13 | from gi.repository import Pango
14 |
15 |
16 | _URI_RE = r'(https?://|/r/|/u/)([^ \r\t\n]+)'
17 | _STRIKE_RE = r'(~~)([^~]+)~~'
18 | _SUPER_RE = r'(\^)(\([^\^\)]+\)|[^\^ ]+)'
19 |
20 |
21 | class _URIPattern(Pattern):
22 | def handleMatch(self, match):
23 | uri = match.group(2) + match.group(3)
24 | el = Element('a')
25 | el.set('href', uri)
26 | el.text = markdown.util.AtomicString(uri)
27 | return el
28 |
29 |
30 | class _SuperPattern(Pattern):
31 | def handleMatch(self, match):
32 | text = match.group(3)
33 | print(text)
34 | if text.startswith('(') and text.endswith(')'):
35 | text = text[1:-1]
36 |
37 | el = Element('sup')
38 | el.text = text
39 | return el
40 |
41 |
42 | class _RedditExtension(Extension):
43 | def extendMarkdown(self, md, md_globals):
44 | md.inlinePatterns['uriregex'] = _URIPattern(_URI_RE, md)
45 |
46 | s_tag = SimpleTagPattern(_STRIKE_RE, 'strike')
47 | md.inlinePatterns.add('s', s_tag, '>not_strong')
48 |
49 | sup_tag = _SuperPattern(_SUPER_RE, md)
50 | md.inlinePatterns.add('sup', sup_tag, '>not_strong')
51 |
52 |
53 | html_pattern = md.inlinePatterns['html']
54 | old = html_pattern.handleMatch
55 | def handleMatch(match):
56 | # Reddit allows interesting markdown, for example this should pass
57 | # through fully: "Hello !"
58 | group_number = 2 if markdown.version_info[0] <= 2 else 1
59 | raw_html = html_pattern.unescape(match.group(2))
60 | try:
61 | root = ElementTree.fromstring(''+raw_html+'
')
62 | except ElementTree.ParseError:
63 | # This is not proper html, so pass it through rather than
64 | # extracting it into an unchangeable stash
65 | return raw_html
66 | else:
67 | # This is proper html, so pass it through
68 | return old(data)
69 | html_pattern.handleMatch = handleMatch
70 |
71 |
72 | MDX_CONTEXT = markdown.Markdown(extensions=[_RedditExtension()])
73 |
74 |
75 | class AlignedLabel(Gtk.Label):
76 | def __init__(self, **kwargs):
77 | super().__init__(
78 | xalign=0,
79 | justify=Gtk.Justification.LEFT,
80 | wrap=True,
81 | wrap_mode=Pango.WrapMode.WORD_CHAR,
82 | **kwargs)
83 |
84 |
85 | HTML_TO_PANGO_INLINE_TAG = {
86 | 'strong': ('', ''),
87 | 'em': ('', ''),
88 | 'strike': ('', ''),
89 | 'sup': ('', ''),
90 | 'br': ('\n', ''),
91 | # We never make these, but sometimes the reddit api does
92 | 'del': ('', ''),
93 | }
94 |
95 | def _html_inline_tag_to_pango(el: Element) -> (str, str):
96 | if el.tag in HTML_TO_PANGO_INLINE_TAG:
97 | return HTML_TO_PANGO_INLINE_TAG[el.tag]
98 | elif el.tag == 'a':
99 | if el.get('href') is None:
100 | return '', ''
101 |
102 | href = html.escape(el.get('href'))
103 | return ''.format(href), ''
104 | else: # pragma: no cover
105 | print('Unknown inline tag', el)
106 | # ElementTree.dump(el)
107 | return '', ''
108 |
109 |
110 | def _make_inline_label(el: Element, initial_text: str = None) -> Gtk.Label:
111 | fragments = [initial_text]
112 |
113 | def extract_text(el, root=False):
114 | if el.tag == 'code':
115 | # special case, as code is processed before rawhtml, so it does not
116 | # get escaped
117 | fragments.append('')
118 | text = ''.join(el.itertext())
119 | fragments.append(html.escape(text))
120 | fragments.append('')
121 | return
122 |
123 | if not root:
124 | start, end = _html_inline_tag_to_pango(el)
125 | fragments.append(start)
126 | fragments.append(html.escape(el.text or '').replace('\n', ''))
127 | for inner in el:
128 | extract_text(inner)
129 | fragments.append(html.escape(inner.tail or '').replace('\n', ''))
130 | if not root:
131 | fragments.append(end)
132 |
133 | extract_text(el, True)
134 |
135 | label = AlignedLabel()
136 | label.connect('activate-link', __activate_link_cb)
137 |
138 | markup = ''.join(x for x in fragments if x is not None)
139 | label.set_markup(markup)
140 |
141 | label.show()
142 | return label
143 |
144 |
145 | def __activate_link_cb(label, uri):
146 | window = label.get_toplevel()
147 | window.load_uri_from_label(uri)
148 | return True
149 |
150 |
151 | BLOCK_TAGS = ['div', 'ol', 'ul', 'blockquote']
152 | HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
153 |
154 |
155 | def _make_li_widget(el: Element, parent: Element, index: int) -> Gtk.Widget:
156 | dot = '⚫ '
157 | if parent is not None and parent.tag == 'ol':
158 | dot = '{}. '.format(index+1)
159 | return _make_inline_label(el, initial_text=dot)
160 |
161 |
162 | def _make_block_widget(el: Element) -> Gtk.Widget:
163 | box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
164 | box.get_style_context().add_class('mdx-block')
165 | box.get_style_context().add_class('mdx-block-'+el.tag)
166 |
167 | assert el.text is None or el.text.strip() == ''
168 | for i, inner in enumerate(el):
169 | assert inner.tail is None or inner.tail.strip() == ''
170 | box.add(convert_tree_to_widgets(inner, parent=el, index=i))
171 |
172 | box.show()
173 | return box
174 |
175 | def _make_heading_widget(el: Element) -> Gtk.Widget:
176 | label = AlignedLabel()
177 | label.props.label = el.text
178 | label.get_style_context().add_class('mdx-heading')
179 | label.get_style_context().add_class('mdx-heading-'+el.tag)
180 | label.show()
181 | return label
182 |
183 |
184 | def _make_code_widget(el: Element) -> Gtk.Widget:
185 | assert len(list(el)) == 1
186 | child = list(el)[0]
187 | code = child.text
188 | assert code is not None
189 |
190 | label = AlignedLabel()
191 | label.props.label = html.unescape(code)
192 | label.get_style_context().add_class('mdx-block-code')
193 | label.show()
194 | return label
195 |
196 |
197 | def convert_tree_to_widgets(el: Element, parent: Element = None,
198 | index: int = None) -> Gtk.Label:
199 | if el.tag == 'p':
200 | return _make_inline_label(el)
201 | elif el.tag == 'li':
202 | return _make_li_widget(el, parent, index)
203 | elif el.tag in BLOCK_TAGS:
204 | return _make_block_widget(el)
205 | elif el.tag in HEADING_TAGS:
206 | return _make_heading_widget(el)
207 | elif el.tag == 'pre':
208 | return _make_code_widget(el)
209 | elif el.tag == 'hr':
210 | return Gtk.Separator(visible=True)
211 | else: # pragma: no cover
212 | print('Unhandled tag', el)
213 | placeholder = Gtk.Spinner()
214 | placeholder.start()
215 | placeholder.show()
216 | return placeholder
217 |
218 |
219 | def make_markdown_widget(text: str) -> Gtk.Widget:
220 | '''
221 | Make a widget of some given text. The markdown widget will be resizable
222 | (it will wrap) and it will probably be a GTK box.
223 |
224 | Args:
225 |
226 | text - markdown text input
227 | '''
228 | html = MDX_CONTEXT.convert(text)
229 | return make_html_widget(html)
230 |
231 |
232 | def make_html_widget(html: str) -> Gtk.Widget:
233 | '''
234 | Make a widget given some html text. Must have a single element as root.
235 | '''
236 | try:
237 | root = ElementTree.fromstring(''+html+'
')
238 | except ElementTree.ParseError as e:
239 | print('Error parsing html,', e, 'for html:')
240 | print(html)
241 | label = AlignedLabel(label='Error formatting text:\n\n{}'.format(html))
242 | label.show()
243 | return label
244 | else:
245 | return convert_tree_to_widgets(root)
246 |
247 |
248 | if __name__ == '__main__':
249 | # Usage:
250 | # python newmarkdown.py test.md
251 | with open(sys.argv[1]) as f:
252 | test_text = f.read()
253 |
254 | window = Gtk.Window()
255 |
256 | w = make_markdown_widget(test_text)
257 | sw = Gtk.ScrolledWindow()
258 | sw.add(w)
259 | sw.show()
260 |
261 | window.add(sw)
262 | window.set_default_size(400, 400)
263 | window.show()
264 | window.connect('delete-event', Gtk.main_quit)
265 | Gtk.main()
266 |
--------------------------------------------------------------------------------
/redditisgtk/palettebutton.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | from gi.repository import GObject
19 | from gi.repository import Gtk
20 |
21 |
22 | class _PaletteButton(GObject.GObject):
23 | '''
24 | Like a Gtk.MenuButton, but instead of requiring the palette to be
25 | constructed initially, it is created on demand by the "create_palette"
26 | method (when subclassed)
27 | '''
28 |
29 | def __init__(self, button, recycle, modalify):
30 | self._button = button
31 | self._recycle = recycle
32 | self._modalify = modalify
33 | self._palette = None
34 | self._self_triggered = False
35 |
36 | self._button.connect('toggled', self.do_toggled)
37 |
38 | def do_toggled(self, button):
39 | if self._self_triggered:
40 | return
41 |
42 | if self._button.props.active:
43 | if self._palette is None:
44 | self._palette = self.create_palette()
45 | self._palette.connect('closed', self.__palette_closed_cb)
46 | self._pc = self._palette.get_child()
47 |
48 | if not self._button.props.visible and self._modalify:
49 | dialog = Gtk.Dialog(use_header_bar=True)
50 | self._palette.remove(self._pc)
51 | dialog.get_content_area().add(self._pc)
52 |
53 | dialog.props.transient_for = self._button.get_toplevel()
54 | dialog.connect('response', self.__dialog_closed_cb)
55 | dialog.show()
56 | else:
57 | self._palette.props.relative_to = button
58 | self._palette.show()
59 | else:
60 | self._palette.hide()
61 | if not self._recycle:
62 | self._palette = None
63 |
64 | def __dialog_closed_cb(self, dialog, response):
65 | dialog.get_content_area().remove(self._pc)
66 | self._palette.add(self._pc)
67 |
68 | self.__palette_closed_cb(None)
69 |
70 | def __palette_closed_cb(self, palette):
71 | self._self_triggered = True
72 | self._button.props.active = False
73 | self._self_triggered = False
74 | if not self._recycle:
75 | self._palette = None
76 |
77 |
78 | def connect_palette(button, create_palette_func, recycle_palette=False,
79 | modalify=False):
80 | p = _PaletteButton(button, recycle_palette, modalify)
81 | p.create_palette = create_palette_func
82 | return p
83 |
--------------------------------------------------------------------------------
/redditisgtk/readcontroller.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | import os
19 | from gi.repository import GLib
20 | from gi.repository import GObject
21 |
22 |
23 | def get_data_file_path(name):
24 | '''
25 | Returns the path for storing user data.
26 |
27 | Makes the directory if it does not exist currently. Does not check
28 | if the file exists.
29 |
30 | Args:
31 | name (str)
32 | '''
33 | data_dir = GLib.get_user_data_dir()
34 | d = os.path.join(data_dir, 'reddit-is-gtk')
35 | if not os.path.isdir(d):
36 | os.makedirs(d)
37 | return os.path.join(d, name)
38 |
39 |
40 | class ReadController(GObject.GObject):
41 |
42 | def __init__(self, data_path: str = None):
43 | GObject.GObject.__init__(self)
44 |
45 | self._set = set([])
46 | self._data_path = data_path
47 | if self._data_path is None:
48 | self._data_path = get_data_file_path('read')
49 |
50 | self.load()
51 |
52 | def read(self, name):
53 | self._set.add(name)
54 | GLib.idle_add(self.save)
55 |
56 | def is_read(self, name):
57 | return name in self._set
58 |
59 | def save(self):
60 | with open(self._data_path, 'w') as f:
61 | for i in self._set:
62 | f.write(i)
63 | f.write('\n')
64 |
65 | def load(self):
66 | if os.path.isfile(self._data_path):
67 | with open(self._data_path) as f:
68 | l = 0
69 | for i in f:
70 | self._set.add(i.strip())
71 | l += 1
72 |
73 |
74 | _ctrl = None
75 |
76 |
77 | def get_read_controller():
78 | global _ctrl
79 | if _ctrl is None:
80 | _ctrl = ReadController()
81 | return _ctrl
82 |
--------------------------------------------------------------------------------
/redditisgtk/settings.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | from gi.repository import Gtk
19 | from gi.repository import Gio
20 |
21 |
22 | _settings = None
23 |
24 |
25 | def get_settings():
26 | '''
27 | Returns our Gio.Settings
28 | '''
29 | global _settings
30 | if _settings is None:
31 | _settings = Gio.Settings(schema='today.sam.something-for-reddit')
32 | return _settings
33 |
34 |
35 | _original_theme_value = None
36 |
37 |
38 | def show_settings():
39 | global _original_theme_value
40 |
41 | builder = Gtk.Builder.new_from_resource(
42 | '/today/sam/reddit-is-gtk/settings-window.ui')
43 | window = builder.get_object('window')
44 | window.show()
45 |
46 |
47 | def __theme_changed_cb(combo):
48 | builder.get_object('restart-warning').props.visible = \
49 | combo.props.active_id != _original_theme_value
50 |
51 | theme_combo = builder.get_object('theme')
52 | get_settings().bind('theme', theme_combo,
53 | 'active-id', Gio.SettingsBindFlags.DEFAULT)
54 | if _original_theme_value is None:
55 | _original_theme_value = theme_combo.props.active_id
56 | __theme_changed_cb(theme_combo)
57 | theme_combo.connect('changed', __theme_changed_cb)
58 |
59 | get_settings().bind('default-sub', builder.get_object('default-sub'),
60 | 'text', Gio.SettingsBindFlags.DEFAULT)
61 |
--------------------------------------------------------------------------------
/redditisgtk/submit.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 | from gi.repository import Gtk
19 | from gi.repository import GObject
20 |
21 | from redditisgtk.api import RedditAPI
22 |
23 |
24 | class SubmitWindow(GObject.GObject):
25 |
26 | done = GObject.Signal('done', arg_types=[str, str])
27 |
28 | def __init__(self, api: RedditAPI, sub=''):
29 | GObject.GObject.__init__(self)
30 | self._api = api
31 |
32 | self._b = Gtk.Builder.new_from_resource(
33 | '/today/sam/reddit-is-gtk/submit-window.ui')
34 | self.window = self._b.get_object('submit-window')
35 |
36 | self._b.get_object('sub-entry').props.text = sub
37 | self._b.get_object('submit-button').connect(
38 | 'clicked', self.__submit_clicked_cb)
39 |
40 | def show(self):
41 | self.window.show()
42 |
43 | def __submit_clicked_cb(self, button):
44 | submit = self._b.get_object('submit-button')
45 | submit.props.label = 'Submitting...'
46 | submit.props.sensitive = False
47 |
48 | data = {'title': self._b.get_object('title-entry').props.text,
49 | 'sr': self._b.get_object('sub-entry').props.text}
50 | stack = self._b.get_object('link-self-stack')
51 | if stack.props.visible_child_name == 'link':
52 | data['kind'] = 'link'
53 | data['url'] = self._b.get_object('link-entry').props.text
54 | else:
55 | data['kind'] = 'self'
56 | buf = self._b.get_object('self-textview').props.buffer
57 | data['text'] = buf.get_text(buf.get_start_iter(),
58 | buf.get_end_iter(), False)
59 | self._api.submit(data, self.__submit_done_cb)
60 |
61 | def __submit_done_cb(self, data):
62 | data = data['json']
63 | if data.get('errors'):
64 | errors = data['errors']
65 | error_name, error_text, error_name_lower = errors[0]
66 |
67 | error = self._b.get_object('error-label')
68 | error.props.label = error_text
69 | error.show()
70 |
71 | submit = self._b.get_object('submit-button')
72 | submit.props.sensitive = True
73 | submit.props.label = 'Submit'
74 | else:
75 | uri = data['data']['url']
76 | self.done.emit(self._b.get_object('sub-entry').props.text,
77 | uri)
78 | self.window.hide()
79 | self.window.destroy()
80 |
--------------------------------------------------------------------------------
/redditisgtk/test_aboutrow.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock, patch
2 |
3 | from gi.repository import Gtk
4 |
5 | from redditisgtk import aboutrow
6 | from redditisgtk.gtktestutil import with_test_mainloop, find_widget, wait_for
7 |
8 |
9 | @with_test_mainloop
10 | def test_subreddit():
11 | api = MagicMock()
12 | root = aboutrow.get_about_row(api, '/r/linux')
13 |
14 |
15 | @with_test_mainloop
16 | @patch('redditisgtk.submit.SubmitWindow', autospec=True)
17 | def test_subreddit_submit(SubmitWindow):
18 | api = MagicMock()
19 | root = aboutrow.get_about_row(api, '/r/linux')
20 |
21 | find_widget(root, label='Submit', kind=Gtk.Button).emit('clicked')
22 | wait_for(lambda: SubmitWindow.called)
23 | SubmitWindow.assert_called_with(api, sub='linux')
24 |
25 |
26 | @with_test_mainloop
27 | def test_subreddit_subscribe_button_initial_state():
28 | api = MagicMock()
29 | api.lower_user_subs = ['/r/sub/']
30 |
31 | root = aboutrow.get_about_row(api, '/r/linux')
32 | find_widget(root, label='Subscribe', kind=Gtk.Button)
33 |
34 | root = aboutrow.get_about_row(api, '/r/sub')
35 | find_widget(root, label='Subscribed', kind=Gtk.Button)
36 |
37 |
38 | @with_test_mainloop
39 | def test_subreddit_subscribe_button_toggle():
40 | api = MagicMock()
41 | root = aboutrow.get_about_row(api, '/r/linux')
42 | btn = find_widget(root, label='Subscribe', kind=Gtk.Button)
43 | btn.emit('clicked')
44 |
45 | wait_for(lambda: api.set_subscribed.called)
46 | (name, active, cb), _ = api.set_subscribed.call_args
47 | assert name == 'linux'
48 | assert active == True
49 |
50 | cb(None)
51 | assert btn == find_widget(root, label='Subscribed', kind=Gtk.Button)
52 |
53 |
54 | @with_test_mainloop
55 | def test_subreddit_info():
56 | api = MagicMock()
57 | root = aboutrow.get_about_row(api, '/r/linux')
58 |
59 | expander = find_widget(root, kind=Gtk.Expander)
60 | expander.activate()
61 | wait_for(lambda: api.get_subreddit_info.called)
62 |
63 | (name, cb), _ = api.get_subreddit_info.call_args
64 | assert name == 'linux'
65 | cb({'data': {'description': 'hello'}})
66 |
67 | assert find_widget(root, label='hello').props.visible == True
68 |
69 | expander.activate()
70 | expander.activate()
71 | assert api.get_subreddit_info.call_count == 1
72 |
73 |
74 | @with_test_mainloop
75 | def test_user_about_row():
76 | api = MagicMock()
77 | root = aboutrow.get_about_row(api, '/u/bob')
78 |
79 | assert find_widget(root, label='bob')
80 |
81 | wait_for(lambda: api.get_user_info.called)
82 | (name, cb), _ = api.get_user_info.call_args
83 | assert name == 'bob'
84 | cb({'data': {'link_karma': 2, 'comment_karma': 1}})
85 |
86 | assert find_widget(root, label='2l / 1c')
87 |
--------------------------------------------------------------------------------
/redditisgtk/test_api.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pytest
3 | from unittest.mock import MagicMock
4 |
5 | from redditisgtk import api
6 |
7 |
8 | def build_fake_soup_session(responses):
9 | '''
10 | Responses is a dictionary of url -> resp
11 |
12 | If resp is a dict, it is returned as json
13 |
14 | If resp is a list, the item is popped and returned as json
15 | '''
16 | session = MagicMock()
17 | def queue_message(msg, callback, user_data):
18 | url = msg.props.uri.to_string(True)
19 |
20 | msg.props = MagicMock()
21 | msg.props.status_code = 200
22 |
23 | resp = responses[url]
24 | if isinstance(resp, list):
25 | resp = resp.pop(0)
26 | data = bytes(json.dumps(resp), 'utf8')
27 | flat = MagicMock()
28 | flat.get_data.return_value = data
29 | msg.props.response_body.flatten.return_value = flat
30 | msg.props.response_body.data = data
31 |
32 | callback(session, msg, user_data)
33 | session.queue_message = queue_message
34 | return session
35 |
36 | def build_fake_api(responses=None, is_anonymous=True):
37 | session = build_fake_soup_session(responses or {})
38 | token = MagicMock()
39 | token.wrap_path = lambda p: 'https://example.com' + p
40 | token.is_anonymous = is_anonymous
41 | return api.RedditAPI(session, token), session, token
42 |
43 | def test_create_api():
44 | api, session, token = build_fake_api()
45 | assert api
46 |
47 | def test_token_changed_gets_username():
48 | api, session, token = build_fake_api({
49 | '/subreddits/mine/subscriber?limit=100&raw_json=1': {
50 | 'data': {
51 | 'children': [],
52 | },
53 | },
54 | '/api/v1/me?raw_json=1': {
55 | 'name': 'myname',
56 | },
57 | }, is_anonymous=False)
58 | assert token.set_user_name.called
59 | (name,), _ = token.set_user_name.call_args
60 | assert name == 'myname'
61 |
62 | token.user_name = 'something'
63 | assert api.user_name is token.user_name
64 |
65 | def test_token_changed_to_annon():
66 | api, session, token = build_fake_api()
67 | assert api.user_name is token.user_name
68 |
69 | def test_update_subscriptions():
70 | api, session, token = build_fake_api({
71 | '/subreddits/mine/subscriber?limit=100&raw_json=1': {
72 | 'data': {
73 | 'children': [{
74 | 'data': {
75 | 'url': '/r/one',
76 | },
77 | }],
78 | },
79 | 'after': 'afterkey',
80 | },
81 | '/subreddits/mine/subscriber?limit=100&after=afterkey&raw_json=1': {
82 | 'data': {
83 | 'children': [{
84 | 'data': {
85 | 'url': '/r/two',
86 | },
87 | }],
88 | },
89 | },
90 | })
91 | api.subs_changed = MagicMock()
92 |
93 | api.update_subscriptions()
94 | assert api.user_subs == ['/r/one', '/r/two']
95 | assert api.lower_user_subs == ['/r/one', '/r/two']
96 | assert api.subs_changed.emit.called
97 |
98 | def test_retry_on_401():
99 | api, session, token = build_fake_api({
100 | '/test?raw_json=1': [
101 | {
102 | 'error': 401,
103 | },
104 | {
105 | 'win': True,
106 | },
107 | ],
108 | })
109 |
110 | done_cb = MagicMock()
111 | api.send_request('GET', '/test', done_cb)
112 | assert token.refresh.called
113 | (inner_cb,), _ = token.refresh.call_args
114 | inner_cb()
115 | assert done_cb.call_count == 1
116 | (data,), _ = done_cb.call_args
117 | assert data == {'win': True}
118 |
119 |
120 | def test_bubble_error():
121 | api, session, token = build_fake_api({
122 | '/test?raw_json=1': {'error': 403},
123 | })
124 |
125 | done_cb = MagicMock()
126 | api.request_failed = MagicMock()
127 | api.send_request('GET', '/test', done_cb)
128 | assert done_cb.call_count == 0
129 |
130 | assert api.request_failed.emit.called
131 | (args, msg), _ = api.request_failed.emit.call_args
132 | assert msg == 'Reddit Error: 403'
133 |
134 |
135 | def test_callback_user_data():
136 | api, session, token = build_fake_api({
137 | '/test?raw_json=1': {'win': True},
138 | })
139 |
140 | done_cb = MagicMock()
141 | ud = 'something'
142 | api.send_request('GET', '/test', done_cb, user_data=ud)
143 | assert done_cb.call_count == 1
144 | (data, ud_ref), _ = done_cb.call_args
145 | assert data == {'win': True}
146 | assert ud_ref is ud
147 |
148 | def test_load_more(datadir):
149 | with open(datadir / 'api__load-more.json') as f:
150 | j = json.load(f)
151 | api, session, token = build_fake_api({
152 | '/api/morechildren?api_type=json&children=a%2Cb&link_id=link_name&raw_json=1': (
153 | j['input']),
154 | })
155 |
156 | done_cb = MagicMock()
157 | api.load_more('link_name', {'children': ['a', 'b']}, done_cb)
158 | assert done_cb.call_count == 1
159 | (data,), _ = done_cb.call_args
160 | assert data == j['output']
161 |
162 | def test_token_manager_base():
163 | tm = api.TokenManager()
164 | with pytest.raises(NotImplementedError):
165 | tm.refresh(lambda: None)
166 | with pytest.raises(NotImplementedError):
167 | tm.wrap_path('')
168 | with pytest.raises(NotImplementedError):
169 | tm.add_message_headers(None)
170 | with pytest.raises(NotImplementedError):
171 | tm.serialize()
172 |
173 | def test_token_anonmyous_refresh():
174 | tm = api.AnonymousTokenManager()
175 | cb = MagicMock()
176 | tm.refresh(cb)
177 | assert cb.called
178 |
179 | def test_token_anonmyous_wrap_path():
180 | tm = api.AnonymousTokenManager()
181 | assert tm.wrap_path('/hello') == 'https://api.reddit.com/hello'
182 |
183 | def test_token_anonmyous_add_message_headers():
184 | tm = api.AnonymousTokenManager()
185 | tm.add_message_headers(None)
186 |
187 | def test_token_oauth_refresh_initial():
188 | session = build_fake_soup_session({
189 | '/api/v1/access_token': {
190 | 'win': 1,
191 | },
192 | })
193 | cb = MagicMock()
194 | tm = api.OAuthTokenManager(session, code='code', ready_callback=cb)
195 | assert cb.called
196 | assert tm.serialize()['win'] == 1
197 |
198 | def test_token_oauth_refresh():
199 | session = build_fake_soup_session({
200 | '/api/v1/access_token': {
201 | 'win': 1,
202 | },
203 | })
204 | cb = MagicMock()
205 | cb2 = MagicMock()
206 | tm = api.OAuthTokenManager(
207 | session,
208 | token={'refresh_token': 'yes', 'win': 0},
209 | ready_callback=cb)
210 | tm.value_changed = MagicMock()
211 | tm.refresh(cb2)
212 | assert cb.called
213 | assert cb2.called
214 | assert tm.serialize()['win'] == 1
215 | assert tm.serialize()['refresh_token'] == 'yes'
216 | assert tm.value_changed.emit.called
217 |
218 | def test_token_oauth_set_username():
219 | tm = api.OAuthTokenManager(None)
220 | tm.value_changed = MagicMock()
221 | assert tm.user_name != 'me'
222 | tm.set_user_name('me')
223 | assert tm.user_name == 'me'
224 | assert tm.serialize()['username'] == 'me'
225 | assert tm.value_changed.emit.called
226 |
227 | def test_token_oauth_add_message_headers():
228 | msg = MagicMock()
229 | tm = api.OAuthTokenManager(None, token={'access_token': 1})
230 | tm.add_message_headers(msg)
231 | args, _ = msg.props.request_headers.append.call_args
232 | assert args == ('Authorization', 'bearer 1')
233 |
234 | def test_api_factory():
235 | factory = api.APIFactory(MagicMock())
236 | token1 = MagicMock()
237 | token2 = MagicMock()
238 | api1 = factory.get_for_token(token1)
239 | api2 = factory.get_for_token(token2)
240 | api1b = factory.get_for_token(token1)
241 | assert api1 is api1b
242 | assert api1 is not api2
243 |
--------------------------------------------------------------------------------
/redditisgtk/test_comments.py:
--------------------------------------------------------------------------------
1 | import json
2 | from unittest.mock import MagicMock
3 |
4 | from gi.repository import Gtk
5 |
6 | from redditisgtk import comments
7 | from redditisgtk.gtktestutil import (with_test_mainloop, find_widget, wait_for,
8 | get_focused, fake_event)
9 |
10 | PERMALINK = '/r/MaliciousCompliance/comments/9vzevr/you_need_a_doctors_note_you_got_it/'
11 |
12 |
13 | @with_test_mainloop
14 | def test_load_selftext(datadir):
15 | api = MagicMock()
16 |
17 | root = comments.CommentsView(api, permalink=PERMALINK)
18 | assert find_widget(root, kind=Gtk.Spinner)
19 |
20 | (method, link, cb), _ = api.send_request.call_args
21 | assert method == 'GET'
22 | assert link == PERMALINK
23 |
24 | with open(datadir / 'comments--thread.json') as f:
25 | cb(json.load(f))
26 |
27 | # Title:
28 | assert find_widget(root, label='You need a doctor’s note? You got it!')
29 | # Body:
30 | assert find_widget(root, label='This happened today.')
31 |
--------------------------------------------------------------------------------
/redditisgtk/test_emptyview.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock
2 |
3 | from gi.repository import Gtk
4 |
5 | from redditisgtk import emptyview
6 | from redditisgtk.gtktestutil import with_test_mainloop, find_widget, wait_for
7 |
8 |
9 | @with_test_mainloop
10 | def test_emptyview_has_label():
11 | root = emptyview.EmptyView('yo')
12 | assert find_widget(root, label='yo')
13 |
14 |
15 | @with_test_mainloop
16 | def test_emptyview_with_action():
17 | root = emptyview.EmptyView('yo', action='do')
18 | root.action = MagicMock()
19 | find_widget(root, label='do', kind=Gtk.Button).emit('clicked')
20 | wait_for(lambda: root.action.emit.called)
21 |
--------------------------------------------------------------------------------
/redditisgtk/test_gtkutil.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock
2 |
3 | from redditisgtk import gtkutil
4 | from redditisgtk.gtktestutil import fake_event
5 |
6 |
7 | def test_process_shortcuts():
8 | up_cb = MagicMock()
9 | k_cb = MagicMock()
10 |
11 | shortcuts = {
12 | 'Up': (up_cb, ['up']),
13 | 'k': (k_cb, ['k']),
14 | }
15 |
16 | gtkutil.process_shortcuts(shortcuts, fake_event('a', event_type=None))
17 | assert not up_cb.called
18 | assert not k_cb.called
19 |
20 | gtkutil.process_shortcuts(shortcuts, fake_event(65362))
21 | assert up_cb.called
22 | assert up_cb.call_args[0] == ('up',)
23 |
24 | gtkutil.process_shortcuts(shortcuts, fake_event('k'))
25 | assert not k_cb.called
26 |
27 | gtkutil.process_shortcuts(shortcuts, fake_event('k', ctrl=True))
28 | assert k_cb.called
29 | assert k_cb.call_args[0] == ('k',)
30 |
--------------------------------------------------------------------------------
/redditisgtk/test_identity.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pytest
3 | import shutil
4 | from unittest.mock import MagicMock, patch
5 |
6 | from redditisgtk import api
7 | from redditisgtk import identity
8 |
9 | def build_identity_controller(path: str = None):
10 | if path is None:
11 | path = '/fake/wow-this-is-a-fake-path'
12 |
13 | session = MagicMock()
14 | ic = identity.IdentityController(session, path)
15 | return ic, session
16 |
17 |
18 | def test_identity_controller_create():
19 | ic, session = build_identity_controller()
20 |
21 |
22 | def test_identity_controller_default_active():
23 | ic, session = build_identity_controller()
24 | assert isinstance(ic.active_token, api.TokenManager)
25 |
26 |
27 | def test_identity_controller_load_file(datadir):
28 | with patch('redditisgtk.api.OAuthTokenManager') as FakeTokenManager:
29 | ic, session = build_identity_controller(
30 | path=datadir / 'identity--load.json')
31 |
32 | assert isinstance(ic.active_token, MagicMock)
33 |
34 | token = FakeTokenManager.call_args[1]['token']
35 | assert token['access_token'] == 'AT'
36 | assert token['username'] == 'myname'
37 |
38 |
39 | def test_token_changed(datadir, tempdir):
40 | path = tempdir / 'i.json'
41 | shutil.copy(datadir / 'identity--load.json', path)
42 |
43 | with patch('redditisgtk.api.OAuthTokenManager') as FakeTokenManager:
44 | ic, session = build_identity_controller(path=path)
45 | ic.token_changed = MagicMock()
46 |
47 | ic.active_token.serialize.return_value = {'win': 1}
48 |
49 | assert ic.active_token.value_changed.connect.called
50 | (callback,), _ = ic.active_token.value_changed.connect.call_args
51 |
52 | callback(ic.active_token)
53 | assert ic.token_changed.emit.called
54 |
55 | with open(path) as f:
56 | data = json.load(f)
57 | assert data['active'] == 'testid'
58 | assert data['tokens']['testid'] == {'win': 1}
59 |
60 |
61 | def test_identity_controller_delete(datadir, tempdir):
62 | path = tempdir / 'i.json'
63 | shutil.copy(datadir / 'identity--load.json', path)
64 |
65 | with patch('redditisgtk.api.OAuthTokenManager') as FakeTokenManager:
66 | ic, session = build_identity_controller(path=path)
67 | ic.token_changed = MagicMock()
68 |
69 | ic.remove_account('testid')
70 |
71 | assert ic.token_changed.emit.called
72 | assert isinstance(ic.active_token, api.AnonymousTokenManager)
73 |
74 | with open(path) as f:
75 | data = json.load(f)
76 | assert data['active'] == None
77 | assert data['tokens'] == {}
78 |
--------------------------------------------------------------------------------
/redditisgtk/test_identitybutton.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock
2 |
3 | from gi.repository import Gtk
4 |
5 | from redditisgtk import identitybutton
6 | from redditisgtk.gtktestutil import with_test_mainloop, find_widget, wait_for
7 |
8 | @with_test_mainloop
9 | def test_button_shows_name():
10 | ic = MagicMock()
11 | ic.active_token.user_name = 'UN1'
12 |
13 | btn = identitybutton.IdentityButton(ic)
14 | assert find_widget(btn, label='UN1', kind=Gtk.Button)
15 |
16 | (cb,), _ = ic.token_changed.connect.call_args
17 | ic.active_token.user_name = 'UN2'
18 | cb(ic)
19 | assert find_widget(btn, label='UN2', kind=Gtk.Button)
20 |
21 |
22 | @with_test_mainloop
23 | def test_popover_lists_accounts():
24 | ic = MagicMock()
25 | ic.all_tokens = [
26 | (1, MagicMock(user_name='user name 1')),
27 | (2, MagicMock(user_name='user name 2'))]
28 |
29 | popover = identitybutton._IdentityPopover(ic)
30 | assert find_widget(popover, label='Anonymous')
31 | assert find_widget(popover, label='user name 1')
32 | assert find_widget(popover, label='user name 2')
33 |
34 | ic.all_tokens = [(1, MagicMock(user_name='user name 1 new'))]
35 | (cb,), _ = ic.token_changed.connect.call_args
36 | cb(ic)
37 | assert find_widget(popover, label='Anonymous')
38 | assert find_widget(popover, label='user name 1 new')
39 | assert find_widget(popover, label='user name 2', many=True) == []
40 | assert find_widget(popover, label='user name 1', many=True) == []
41 |
42 | @with_test_mainloop
43 | def test_popover_selects_row():
44 | ic = MagicMock()
45 | ic.all_tokens = [
46 | (1, MagicMock(user_name='user name 1')),
47 | (2, MagicMock(user_name='user name 2'))]
48 | ic.active_token = ic.all_tokens[0][1]
49 |
50 | popover = identitybutton._IdentityPopover(ic)
51 |
52 | def get_row(text: str):
53 | label = find_widget(popover, label=text)
54 | while not isinstance(label, Gtk.ListBoxRow):
55 | assert label
56 | label = label.get_parent()
57 | return label
58 |
59 | row1 = get_row('user name 1')
60 | row2 = get_row('user name 2')
61 | listbox = find_widget(popover, kind=Gtk.ListBox)
62 |
63 | assert listbox.get_selected_rows() == [row1]
64 | listbox.emit('row-selected', row2)
65 | wait_for(lambda: ic.switch_account.called)
66 | assert ic.switch_account.call_args[0][0] == 2
67 |
--------------------------------------------------------------------------------
/redditisgtk/test_newmarkdown.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock
2 |
3 | from redditisgtk import newmarkdown
4 | from redditisgtk.gtktestutil import (with_test_mainloop, snapshot_widget,
5 | find_widget)
6 | from redditisgtk.conftest import assert_matches_snapshot
7 |
8 |
9 | def test_hello():
10 | widget = newmarkdown.make_markdown_widget('hello')
11 | assert_matches_snapshot('newmarkdown--hello', snapshot_widget(widget))
12 |
13 |
14 | def test_inline():
15 | widget = newmarkdown.make_markdown_widget('**b** _i_ ~~s~~ ``')
16 | assert_matches_snapshot('newmarkdown--inline', snapshot_widget(widget))
17 |
18 |
19 | def test_super():
20 | w = newmarkdown.make_markdown_widget('hello ^my world')
21 | assert_matches_snapshot('newmarkdown--super', snapshot_widget(w))
22 |
23 | w = newmarkdown.make_markdown_widget('hello ^(woah long) world')
24 | assert_matches_snapshot('newmarkdown--super-long', snapshot_widget(w))
25 |
26 |
27 | def test_blockquote():
28 | widget = newmarkdown.make_markdown_widget('''
29 | hello world
30 |
31 | > **quoted**
32 | >
33 | > > sub quote
34 | ''')
35 | assert_matches_snapshot('newmarkdown--quote', snapshot_widget(widget))
36 |
37 |
38 | def test_hr():
39 | widget = newmarkdown.make_markdown_widget('''
40 | hello
41 |
42 | ---
43 |
44 | world
45 | ''')
46 | assert_matches_snapshot('newmarkdown--hr', snapshot_widget(widget))
47 |
48 |
49 | def test_heading():
50 | widget = newmarkdown.make_markdown_widget('''
51 | # big
52 | ### small
53 | ''')
54 | assert_matches_snapshot('newmarkdown--heading', snapshot_widget(widget))
55 |
56 |
57 | def test_list():
58 | widget = newmarkdown.make_markdown_widget('''
59 | 1. hello
60 | 3. world
61 |
62 | * yeah
63 | * sam
64 | ''')
65 | assert_matches_snapshot('newmarkdown--list', snapshot_widget(widget))
66 |
67 |
68 | def test_link():
69 | widget = newmarkdown.make_markdown_widget('/r/linux')
70 | assert_matches_snapshot('newmarkdown--link--r', snapshot_widget(widget))
71 |
72 | cb = MagicMock()
73 | label = find_widget(widget, label='/r/linux')
74 | label.get_toplevel().load_uri_from_label = cb
75 |
76 | label.emit('activate-link', '/r/linux')
77 | assert cb.called
78 | (link,), _ = cb.call_args
79 | assert link == '/r/linux'
80 |
81 |
82 | def test_code():
83 | widget = newmarkdown.make_markdown_widget('''
84 | hello:
85 |
86 |
87 |
88 | ''')
89 | assert_matches_snapshot('newmarkdown--code', snapshot_widget(widget))
90 |
91 |
92 | def test_escaping():
93 | # This is "valid", e.g.
94 | # https://www.reddit.com/r/programmingcirclejerk/comments/9v7ix9/github_cee_lo_green_implements_fk_you_as_a_feature/e9adb06/
95 | widget = newmarkdown.make_markdown_widget('Please refer to for')
96 | assert_matches_snapshot('newmarkdown--escaping', snapshot_widget(widget))
97 |
98 | w = newmarkdown.make_markdown_widget('Code: ``')
99 | assert_matches_snapshot('newmarkdown--escaping-code', snapshot_widget(w))
100 |
101 | w = newmarkdown.make_markdown_widget('& I <3 you')
102 | assert_matches_snapshot('newmarkdown--escaping-amp', snapshot_widget(w))
103 |
104 | def test_error():
105 | # OK so this comment is no longer valid, so I'm not going to bother to fix
106 | # things for it:
107 | # https://www.reddit.com/r/reddit.com/comments/6ewgt/reddit_markdown_primer_or_how_do_you_do_all_that/c03nmy1/
108 | w = newmarkdown.make_html_widget('Issue ™
')
109 | assert_matches_snapshot('newmarkdown--error-handler', snapshot_widget(w))
110 |
--------------------------------------------------------------------------------
/redditisgtk/test_posttopbar.py:
--------------------------------------------------------------------------------
1 | import json
2 | from unittest.mock import MagicMock, patch
3 |
4 | from gi.repository import Gtk
5 |
6 | from redditisgtk import posttopbar
7 | from redditisgtk.gtktestutil import (with_test_mainloop, find_widget, wait_for,
8 | fake_event)
9 |
10 |
11 | @with_test_mainloop
12 | def test_for_comment(datadir):
13 | api = MagicMock()
14 | toplevel_cv = MagicMock()
15 |
16 | with open(datadir / 'posttopbar--comment.json') as f:
17 | data = json.load(f)
18 |
19 | bar = posttopbar.PostTopBar(api, data, toplevel_cv)
20 | # author
21 | assert find_widget(bar, label='andnbspsc', kind=Gtk.Button)
22 | # score
23 | assert find_widget(bar, label='score hidden', kind=Gtk.Button)
24 | # no subreddit
25 | assert not find_widget(bar, label='linux', many=True)
26 |
27 |
28 | @with_test_mainloop
29 | def test_for_post(datadir):
30 | api = MagicMock()
31 | toplevel_cv = MagicMock()
32 |
33 | with open(datadir / 'posttopbar--post.json') as f:
34 | data = json.load(f)
35 |
36 | bar = posttopbar.PostTopBar(api, data, toplevel_cv, show_subreddit=True)
37 | # author
38 | assert find_widget(bar, label='sandragen', kind=Gtk.Button)
39 | # score
40 | assert find_widget(bar, label='score hidden', kind=Gtk.Button)
41 | # subreddit
42 | assert find_widget(bar, label='linux', many=True)
43 |
44 |
45 | @with_test_mainloop
46 | def test_vote_key(datadir):
47 | api = MagicMock()
48 | toplevel_cv = MagicMock()
49 | with open(datadir / 'posttopbar--comment.json') as f:
50 | data = json.load(f)
51 |
52 | bar = posttopbar.PostTopBar(api, data, toplevel_cv)
53 | bar.get_toplevel = lambda: api
54 | bar.do_event(fake_event('u'))
55 | assert api.vote.call_args[0] == ('t1_e9nhj7n', +1)
56 | bar.do_event(fake_event('d'))
57 | assert api.vote.call_args[0] == ('t1_e9nhj7n', -1)
58 | bar.do_event(fake_event('n'))
59 | assert api.vote.call_args[0] == ('t1_e9nhj7n', 0)
60 |
61 |
62 | @with_test_mainloop
63 | def test_reply_palette(datadir):
64 | api = MagicMock()
65 | toplevel_cv = MagicMock()
66 | with open(datadir / 'posttopbar--comment.json') as f:
67 | data = json.load(f)
68 |
69 | bar = posttopbar.PostTopBar(api, data, toplevel_cv)
70 | bar.get_toplevel = lambda: api
71 | poproot = Gtk.Popover()
72 | with patch('gi.repository.Gtk.Popover') as Popover:
73 | Popover.return_value = poproot
74 | bar.do_event(fake_event('r'))
75 |
76 | wait_for(lambda: Popover.called)
77 |
78 | assert poproot.props.visible
79 | tv = find_widget(poproot, kind=Gtk.TextView)
80 | tv.props.buffer.set_text('hello')
81 |
82 | btn = find_widget(poproot, kind=Gtk.Button, label='Post Reply')
83 | btn.emit('clicked')
84 | wait_for(lambda: btn.props.sensitive is False)
85 | (name, text, cb), _ = api.reply.call_args
86 | assert name == 't1_e9nhj7n'
87 | assert text == 'hello'
88 |
89 | cb({'json': {'data': {'things': [{'data': {'id': 'MYID'}}]}}})
90 | assert poproot.props.visible == False
91 |
--------------------------------------------------------------------------------
/redditisgtk/test_readcontroller.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock, patch
2 |
3 | from redditisgtk import readcontroller
4 |
5 | @patch('os.makedirs')
6 | @patch('os.path.isdir', return_value=False)
7 | def test_get_data_file_path(isdir, makedirs):
8 | path = readcontroller.get_data_file_path('name')
9 | assert path.endswith('name')
10 | assert path.startswith('/')
11 | assert makedirs.called
12 |
13 |
14 | @patch('os.makedirs')
15 | @patch('os.path.isdir', return_value=True)
16 | def test_get_data_file_path_no_create(isdir, makedirs):
17 | path = readcontroller.get_data_file_path('name')
18 | assert not makedirs.called
19 |
20 |
21 | def test_readcontroller_load(tmpdir):
22 | path = tmpdir / 'read'
23 | with open(path, 'w') as f:
24 | f.write('a1\na2')
25 | ctrl = readcontroller.ReadController(data_path=path)
26 | assert ctrl.is_read('a1')
27 | assert ctrl.is_read('a2')
28 | assert not ctrl.is_read('b1')
29 |
30 | ctrl.read('b1')
31 | assert ctrl.is_read('b1')
32 |
33 | ctrl.save()
34 | with open(path) as f:
35 | assert sorted(f.read().splitlines()) == ['a1', 'a2', 'b1']
36 |
--------------------------------------------------------------------------------
/redditisgtk/test_subentry.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock
2 |
3 | from gi.repository import Gtk
4 |
5 | from redditisgtk import subentry
6 | from redditisgtk.gtktestutil import (with_test_mainloop, find_widget, wait_for,
7 | fake_event)
8 |
9 |
10 | def test_clean_sub():
11 | assert subentry.clean_sub('') == '/'
12 | assert subentry.clean_sub('hello') == '/hello'
13 | assert subentry.clean_sub('/u/sam') == '/user/sam'
14 |
15 |
16 | def test_clean_sub_passes_through_uris():
17 | assert subentry.clean_sub('https://') == 'https://'
18 | assert subentry.clean_sub('http://reddit.com') == 'http://reddit.com'
19 |
20 |
21 | def test_format_sub_for_api_frontpage():
22 | assert subentry.format_sub_for_api('') == '/hot'
23 | assert subentry.format_sub_for_api('/') == '/hot'
24 | assert subentry.format_sub_for_api('/top') == '/top'
25 |
26 |
27 | def test_format_sub_for_api_subreddit():
28 | assert subentry.format_sub_for_api('r/linux') == '/r/linux/hot'
29 | assert subentry.format_sub_for_api('/r/l/top?t=all') == '/r/l/top?t=all'
30 |
31 |
32 | def test_format_sub_for_api_user():
33 | assert subentry.format_sub_for_api('/u/sam') == '/user/sam/overview'
34 | assert subentry.format_sub_for_api('/u/sam/up') == '/user/sam/up'
35 |
36 |
37 | @with_test_mainloop
38 | def test_subentry_create():
39 | api = MagicMock()
40 | api.user_name = 'username'
41 | root = subentry.SubEntry(api, text='/r/linux')
42 |
43 | assert find_widget(root, label='/r/linux')
44 |
45 |
46 | @with_test_mainloop
47 | def test_subentry_palette_activate():
48 | api = MagicMock()
49 | api.user_name = 'username'
50 | root = subentry.SubEntry(api)
51 | root.activate = MagicMock()
52 |
53 | down_button = find_widget(root, kind=Gtk.Button)
54 | down_button.emit('clicked')
55 | poproot = root._palette # err IDK about this
56 | wait_for(lambda: poproot.props.visible)
57 |
58 | btn = find_widget(
59 | poproot, label='/user/username/submitted', kind=Gtk.Button)
60 | btn.emit('clicked')
61 | wait_for(lambda: root.activate.emit.called)
62 | assert root.activate.emit.call_args[0][0] == '/user/username/submitted'
63 |
64 |
65 | @with_test_mainloop
66 | def test_subentry_palette_subreddits():
67 | api = MagicMock()
68 | api.user_name = 'username'
69 | api.user_subs = ['/r/linux']
70 | root = subentry.SubEntry(api)
71 |
72 | down_button = find_widget(root, kind=Gtk.Button)
73 | down_button.emit('clicked')
74 | poproot = root._palette # err IDK about this
75 | wait_for(lambda: poproot.props.visible)
76 |
77 | assert find_widget(poproot, label='/r/linux', many=True)
78 | assert not find_widget(poproot, label='/r/gnu', many=True)
79 |
80 | api.user_subs = ['/r/gnu']
81 | (cb,), _ = api.subs_changed.connect.call_args
82 | cb(api)
83 | assert not find_widget(poproot, label='/r/linux', many=True)
84 | assert find_widget(poproot, label='/r/gnu', many=True)
85 |
86 |
87 | @with_test_mainloop
88 | def test_subentry_open_uri():
89 | api = MagicMock()
90 | api.user_name = 'username'
91 | root = subentry.SubEntry(api)
92 | toplevel = MagicMock()
93 |
94 | entry = find_widget(root, kind=Gtk.Entry)
95 | # err IDK about this
96 | entry.is_focus = lambda: True
97 | entry.props.text = 'https://reddit.com/r/yes'
98 |
99 | poproot = root._palette # err IDK about this
100 | poproot.get_toplevel = lambda: toplevel
101 | wait_for(lambda: poproot.props.visible)
102 |
103 | btn = find_widget(poproot, label='Open this reddit.com URI',
104 | kind=Gtk.Button)
105 | btn.emit('clicked')
106 | wait_for(lambda: toplevel.goto_reddit_uri.called)
107 | toplevel.goto_reddit_uri.assert_called_once_with(
108 | 'https://reddit.com/r/yes')
109 |
110 |
111 | @with_test_mainloop
112 | def test_subentry_palette_subreddits_filter():
113 | api = MagicMock()
114 | api.user_name = 'username'
115 | api.user_subs = ['/r/linux', '/r/gnu']
116 | root = subentry.SubEntry(api)
117 | poproot = root._palette # err IDK about this
118 |
119 | entry = find_widget(root, kind=Gtk.Entry)
120 | entry.props.text = '/r/l'
121 |
122 |
123 | # When using the button, all should be visible
124 | down_button = find_widget(root, kind=Gtk.Button)
125 | down_button.emit('clicked')
126 | wait_for(lambda: poproot.props.visible)
127 |
128 | assert find_widget(poproot, label='/r/linux', many=True)
129 | assert find_widget(poproot, label='/r/gnu', many=True)
130 |
131 | # err IDK about this
132 | entry.is_focus = lambda: True
133 | entry.props.text = '/r/li'
134 | wait_for(lambda: poproot.props.visible)
135 |
136 | assert find_widget(poproot, label='/r/linux', many=True)
137 | assert not find_widget(poproot, label='/r/gnu', many=True)
138 |
--------------------------------------------------------------------------------
/redditisgtk/test_sublist.py:
--------------------------------------------------------------------------------
1 | import json
2 | from unittest.mock import MagicMock, patch
3 |
4 | from gi.repository import Gtk
5 |
6 | from redditisgtk import sublist
7 | from redditisgtk.gtktestutil import (with_test_mainloop, find_widget, wait_for,
8 | fake_event)
9 |
10 |
11 | class FixtureMessageRow(Gtk.Label):
12 | def __init__(self, api, data):
13 | assert data['win']
14 | Gtk.Label.__init__(self, label='message row')
15 |
16 |
17 | class FixtureLinkRow(Gtk.Label):
18 | goto_comments = MagicMock()
19 |
20 | def __init__(self, api, data):
21 | assert data['win']
22 | Gtk.Label.__init__(self, label='link row')
23 |
24 |
25 | class FixtureMoreItemsRow(Gtk.Label):
26 | load_more = MagicMock()
27 |
28 | def __init__(self, data):
29 | assert data == 'win'
30 | Gtk.Label.__init__(self, label='more items row')
31 |
32 |
33 | @with_test_mainloop
34 | @patch('redditisgtk.aboutrow.get_about_row',
35 | return_value=Gtk.Label(label='about row'))
36 | @patch('redditisgtk.sublistrows.MessageRow', FixtureMessageRow)
37 | @patch('redditisgtk.sublistrows.LinkRow', FixtureLinkRow)
38 | @patch('redditisgtk.sublistrows.MoreItemsRow', FixtureMoreItemsRow)
39 | def test_sublist_create(get_about_row):
40 | api = MagicMock()
41 | root = sublist.SubList(api, '/r/linux')
42 |
43 | assert find_widget(root, kind=Gtk.Spinner)
44 | assert root.get_uri() == '/r/linux'
45 |
46 | (sub, cb), _ = api.get_list.call_args
47 | assert sub == '/r/linux'
48 | data = {
49 | 'data': {
50 | 'children': [
51 | {'kind': 't1', 'win': 1},
52 | {'kind': 't3', 'win': 1},
53 | ],
54 | 'after': 'win',
55 | },
56 | }
57 | # Load the data
58 | cb(data)
59 | assert not find_widget(root, kind=Gtk.Spinner, many=True)
60 |
61 | get_about_row.assert_called_with(api, '/r/linux')
62 | assert find_widget(root, label='about row', kind=Gtk.Label)
63 | assert find_widget(root, kind=FixtureMessageRow)
64 | assert find_widget(root, kind=FixtureLinkRow)
65 | assert find_widget(root, kind=FixtureMoreItemsRow)
66 |
--------------------------------------------------------------------------------
/redditisgtk/test_sublistrows.py:
--------------------------------------------------------------------------------
1 | import json
2 | from unittest.mock import MagicMock, patch
3 |
4 | from gi.repository import Gtk
5 |
6 | from redditisgtk import sublistrows
7 | from redditisgtk.gtktestutil import (with_test_mainloop, find_widget, wait_for,
8 | fake_event)
9 |
10 | @with_test_mainloop
11 | def test_subitemrow_thumb_from_preview(json_loader):
12 | api = MagicMock()
13 | row = sublistrows.LinkRow(api,
14 | json_loader('sublistrows--thumb-from-previews'))
15 |
16 | assert api.download_thumb.called
17 | (url, cb), _ = api.download_thumb.call_args
18 | assert url == 'https://external-preview.redd.it/AxVEZvB8AZsAPFSlrGM3HGDCss0bxYEbNu89NmgUdTg.jpg?width=108&crop=smart&auto=webp&s=a906c3a7241def971591ded4d7f9ff9abe6b050c'
19 |
20 |
21 | @with_test_mainloop
22 | def test_message_row_pm(json_loader):
23 | api = MagicMock()
24 | row = sublistrows.MessageRow(api, json_loader('sublistrows--pm'))
25 |
26 | assert find_widget(row, label='re: Blog down')
27 | assert find_widget(row, label='thanks a lot!')
28 | # author:
29 | assert find_widget(row, label='bambambazooka', kind=Gtk.Button)
30 |
31 |
32 | @with_test_mainloop
33 | def test_message_row_pm_shortcuts(json_loader):
34 | api = MagicMock()
35 | toplevel = MagicMock()
36 |
37 | row = sublistrows.MessageRow(api, json_loader('sublistrows--pm'))
38 | row.get_toplevel = lambda: toplevel
39 |
40 | row.do_event(fake_event('a'))
41 | toplevel.goto_sublist.assert_called_once_with('/u/bambambazooka')
42 |
--------------------------------------------------------------------------------
/redditisgtk/test_submit.py:
--------------------------------------------------------------------------------
1 | import json
2 | from unittest.mock import MagicMock
3 |
4 | from gi.repository import Gtk
5 |
6 | from redditisgtk import submit
7 | from redditisgtk.gtktestutil import with_test_mainloop, find_widget, wait_for
8 |
9 |
10 | @with_test_mainloop
11 | def test_create_window():
12 | api = MagicMock()
13 | window = submit.SubmitWindow(api)
14 | root = window.window
15 | assert find_widget(root, placeholder='Subreddit').props.text == ''
16 |
17 |
18 | @with_test_mainloop
19 | def test_create_window_with_subreddit():
20 | api = MagicMock()
21 | window = submit.SubmitWindow(api, sub='linux')
22 | root = window.window
23 | assert find_widget(root, placeholder='Subreddit').props.text == 'linux'
24 |
25 |
26 | @with_test_mainloop
27 | def test_submit_empty():
28 | api = MagicMock()
29 | window = submit.SubmitWindow(api)
30 | root = window.window
31 |
32 | submit_button = find_widget(root, label='Submit', kind=Gtk.Button)
33 | submit_button.emit('clicked')
34 |
35 | wait_for(lambda: api.submit.called)
36 |
37 | loading = find_widget(root, label='Submitting...', kind=Gtk.Button)
38 | assert loading.props.sensitive == False
39 |
40 |
41 | @with_test_mainloop
42 | def test_submit_link():
43 | api = MagicMock()
44 | window = submit.SubmitWindow(api)
45 | root = window.window
46 |
47 | find_widget(root, placeholder='Title').props.text = 'Some Title'
48 | find_widget(root, placeholder='Link').props.text = 'example.com'
49 | find_widget(root, placeholder='Subreddit').props.text = 'test'
50 | find_widget(root, label='Submit', kind=Gtk.Button).emit('clicked')
51 |
52 | wait_for(lambda: api.submit.called)
53 | (data, cb), _ = api.submit.call_args
54 | assert data == {
55 | 'kind': 'link',
56 | 'sr': 'test',
57 | 'title': 'Some Title',
58 | 'url': 'example.com',
59 | }
60 |
61 |
62 | @with_test_mainloop
63 | def test_submit_self():
64 | api = MagicMock()
65 | window = submit.SubmitWindow(api)
66 | root = window.window
67 |
68 | self_post = find_widget(root, kind=Gtk.Button, label='Self Post')
69 | self_post.props.active = True
70 | stack = find_widget(root, kind=Gtk.Stack)
71 | wait_for(lambda: stack.props.visible_child_name == 'self')
72 |
73 | find_widget(root, placeholder='Title').props.text = 'Some Title'
74 | find_widget(root, kind=Gtk.TextView).props.buffer.set_text('self')
75 | find_widget(root, placeholder='Subreddit').props.text = 'test'
76 | find_widget(root, label='Submit', kind=Gtk.Button).emit('clicked')
77 |
78 | wait_for(lambda: api.submit.called)
79 | (data, cb), _ = api.submit.call_args
80 | assert data == {
81 | 'kind': 'self',
82 | 'sr': 'test',
83 | 'title': 'Some Title',
84 | 'text': 'self',
85 | }
86 |
87 | @with_test_mainloop
88 | def test_submit_error(datadir):
89 | api = MagicMock()
90 | window = submit.SubmitWindow(api)
91 | root = window.window
92 |
93 | submit_button = find_widget(root, label='Submit', kind=Gtk.Button)
94 | submit_button.emit('clicked')
95 | wait_for(lambda: api.submit.called)
96 | (data, callback), _ = api.submit.call_args
97 |
98 | with open(datadir / 'submit--ratelimit-response.json') as f:
99 | data = json.load(f)
100 | msg = data['json']['errors'][0][1]
101 | callback(data)
102 |
103 | assert submit_button.props.sensitive
104 | label = find_widget(root, label=msg)
105 | assert label.props.visible
106 |
107 |
108 | @with_test_mainloop
109 | def test_submit_good(datadir):
110 | api = MagicMock()
111 | window = submit.SubmitWindow(api)
112 | window.done = MagicMock()
113 | root = window.window
114 |
115 | submit_button = find_widget(root, label='Submit', kind=Gtk.Button)
116 | submit_button.emit('clicked')
117 | wait_for(lambda: api.submit.called)
118 | (data, callback), _ = api.submit.call_args
119 |
120 | with open(datadir / 'submit--good-response.json') as f:
121 | callback(json.load(f))
122 |
123 | assert window.done.emit.called
124 | (sub, uri), _ = window.done.emit.call_args
125 | assert uri == 'https://www.reddit.com/r/test/comments/9teb69/test/'
126 |
--------------------------------------------------------------------------------
/redditisgtk/tests-data/identity--load.json:
--------------------------------------------------------------------------------
1 | {"tokens": {"testid": {"access_token": "AT", "token_type": "bearer", "expires_in": 3600, "refresh_token": "RT", "scope": "edit history identity mysubreddits privatemessages read save submit subscribe vote", "time": 1540967369.7608967, "username": "myname"}}, "active": "testid"}
2 |
--------------------------------------------------------------------------------
/redditisgtk/tests-data/posttopbar--comment.json:
--------------------------------------------------------------------------------
1 | {"subreddit_id": "t5_2qh1a", "approved_at_utc": null, "ups": 1, "mod_reason_by": null, "banned_by": null, "author_flair_type": "text", "removal_reason": null, "link_id": "t3_9wuoss", "author_flair_template_id": null, "likes": null, "no_follow": true, "replies": "", "user_reports": [], "saved": false, "id": "e9nhj7n", "banned_at_utc": null, "mod_reason_title": null, "gilded": 0, "archived": false, "report_reasons": null, "author": "andnbspsc", "can_mod_post": false, "send_replies": true, "parent_id": "t1_e9nfjwa", "score": 1, "author_fullname": "t2_zlqp1", "approved_by": null, "downs": 0, "body": "Oh woops ok bye!", "edited": false, "author_flair_css_class": null, "is_submitter": true, "collapsed": false, "author_flair_richtext": [], "author_patreon_flair": false, "collapsed_reason": null, "body_html": "", "stickied": false, "subreddit_type": "public", "can_gild": true, "gildings": {"gid_1": 0, "gid_2": 0, "gid_3": 0}, "author_flair_text_color": null, "score_hidden": true, "permalink": "/r/linux/comments/9wuoss/keyboard_shortcuts_between_terminal_and_other_gui/e9nhj7n/", "num_reports": null, "name": "t1_e9nhj7n", "created": 1542185030.0, "subreddit": "linux", "author_flair_text": null, "created_utc": 1542156230.0, "subreddit_name_prefixed": "r/linux", "controversiality": 0, "depth": 1, "author_flair_background_color": null, "mod_reports": [], "mod_note": null, "distinguished": null}
2 |
--------------------------------------------------------------------------------
/redditisgtk/tests-data/posttopbar--post.json:
--------------------------------------------------------------------------------
1 | {"subreddit_id": "t5_2qh1a", "approved_at_utc": null, "ups": 1, "mod_reason_by": null, "banned_by": null, "author_flair_type": "text", "removal_reason": null, "link_id": "t3_9wuoss", "author_flair_template_id": null, "likes": null, "no_follow": true, "replies": {"kind": "Listing", "data": {"modhash": null, "dist": null, "children": [{"kind": "t1", "data": {"subreddit_id": "t5_2qh1a", "approved_at_utc": null, "ups": 1, "mod_reason_by": null, "banned_by": null, "author_flair_type": "text", "removal_reason": null, "link_id": "t3_9wuoss", "author_flair_template_id": null, "likes": null, "no_follow": true, "replies": "", "user_reports": [], "saved": false, "id": "e9nhj7n", "banned_at_utc": null, "mod_reason_title": null, "gilded": 0, "archived": false, "report_reasons": null, "author": "andnbspsc", "can_mod_post": false, "send_replies": true, "parent_id": "t1_e9nfjwa", "score": 1, "author_fullname": "t2_zlqp1", "approved_by": null, "downs": 0, "body": "Oh woops ok bye!", "edited": false, "author_flair_css_class": null, "is_submitter": true, "collapsed": false, "author_flair_richtext": [], "author_patreon_flair": false, "collapsed_reason": null, "body_html": "", "stickied": false, "subreddit_type": "public", "can_gild": true, "gildings": {"gid_1": 0, "gid_2": 0, "gid_3": 0}, "author_flair_text_color": null, "score_hidden": true, "permalink": "/r/linux/comments/9wuoss/keyboard_shortcuts_between_terminal_and_other_gui/e9nhj7n/", "num_reports": null, "name": "t1_e9nhj7n", "created": 1542185030.0, "subreddit": "linux", "author_flair_text": null, "created_utc": 1542156230.0, "subreddit_name_prefixed": "r/linux", "controversiality": 0, "depth": 1, "author_flair_background_color": null, "mod_reports": [], "mod_note": null, "distinguished": null}}], "after": null, "before": null}}, "user_reports": [], "saved": false, "id": "e9nfjwa", "banned_at_utc": null, "mod_reason_title": null, "gilded": 0, "archived": false, "report_reasons": null, "author": "sandragen", "can_mod_post": false, "send_replies": true, "parent_id": "t3_9wuoss", "score": 1, "author_fullname": "t2_hwtp5", "approved_by": null, "downs": 0, "body": "Rule 1", "edited": false, "author_flair_css_class": null, "is_submitter": false, "collapsed": false, "author_flair_richtext": [], "author_patreon_flair": false, "collapsed_reason": null, "body_html": "", "stickied": false, "subreddit_type": "public", "can_gild": true, "gildings": {"gid_1": 0, "gid_2": 0, "gid_3": 0}, "author_flair_text_color": null, "score_hidden": true, "permalink": "/r/linux/comments/9wuoss/keyboard_shortcuts_between_terminal_and_other_gui/e9nfjwa/", "num_reports": null, "name": "t1_e9nfjwa", "created": 1542183175.0, "subreddit": "linux", "author_flair_text": null, "created_utc": 1542154375.0, "subreddit_name_prefixed": "r/linux", "controversiality": 0, "depth": 0, "author_flair_background_color": null, "mod_reports": [], "mod_note": null, "distinguished": null}
2 |
3 |
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--code.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "hello:",
6 | "type": "AlignedLabel"
7 | },
8 | {
9 | "classes": [
10 | "mdx-block-code"
11 | ],
12 | "label": "\n\n",
13 | "type": "AlignedLabel"
14 | }
15 | ],
16 | "classes": [
17 | "vertical",
18 | "mdx-block",
19 | "mdx-block-div"
20 | ],
21 | "label": null,
22 | "type": "Box"
23 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--error-handler.json:
--------------------------------------------------------------------------------
1 | {
2 | "classes": [],
3 | "label": "Error formatting text:\n\nIssue ™
",
4 | "type": "AlignedLabel"
5 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--escaping-amp.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "& I <3 you",
6 | "type": "AlignedLabel"
7 | }
8 | ],
9 | "classes": [
10 | "vertical",
11 | "mdx-block",
12 | "mdx-block-div"
13 | ],
14 | "label": null,
15 | "type": "Box"
16 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--escaping-code.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "Code: <hello></hello>",
6 | "type": "AlignedLabel"
7 | }
8 | ],
9 | "classes": [
10 | "vertical",
11 | "mdx-block",
12 | "mdx-block-div"
13 | ],
14 | "label": null,
15 | "type": "Box"
16 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--escaping.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "Please refer to <url> for",
6 | "type": "AlignedLabel"
7 | }
8 | ],
9 | "classes": [
10 | "vertical",
11 | "mdx-block",
12 | "mdx-block-div"
13 | ],
14 | "label": null,
15 | "type": "Box"
16 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--heading.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [
5 | "mdx-heading",
6 | "mdx-heading-h1"
7 | ],
8 | "label": "big",
9 | "type": "AlignedLabel"
10 | },
11 | {
12 | "classes": [
13 | "mdx-heading",
14 | "mdx-heading-h3"
15 | ],
16 | "label": "small",
17 | "type": "AlignedLabel"
18 | }
19 | ],
20 | "classes": [
21 | "vertical",
22 | "mdx-block",
23 | "mdx-block-div"
24 | ],
25 | "label": null,
26 | "type": "Box"
27 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--hello.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "hello",
6 | "type": "AlignedLabel"
7 | }
8 | ],
9 | "classes": [
10 | "vertical",
11 | "mdx-block",
12 | "mdx-block-div"
13 | ],
14 | "label": null,
15 | "type": "Box"
16 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--hr.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "hello",
6 | "type": "AlignedLabel"
7 | },
8 | {
9 | "classes": [
10 | "horizontal"
11 | ],
12 | "label": null,
13 | "type": "Separator"
14 | },
15 | {
16 | "classes": [],
17 | "label": "world",
18 | "type": "AlignedLabel"
19 | }
20 | ],
21 | "classes": [
22 | "vertical",
23 | "mdx-block",
24 | "mdx-block-div"
25 | ],
26 | "label": null,
27 | "type": "Box"
28 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--inline.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "b i s <code>",
6 | "type": "AlignedLabel"
7 | }
8 | ],
9 | "classes": [
10 | "vertical",
11 | "mdx-block",
12 | "mdx-block-div"
13 | ],
14 | "label": null,
15 | "type": "Box"
16 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--link--r.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "/r/linux",
6 | "type": "AlignedLabel"
7 | }
8 | ],
9 | "classes": [
10 | "vertical",
11 | "mdx-block",
12 | "mdx-block-div"
13 | ],
14 | "label": null,
15 | "type": "Box"
16 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--list.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "children": [
5 | {
6 | "classes": [],
7 | "label": "1. hello",
8 | "type": "AlignedLabel"
9 | },
10 | {
11 | "classes": [],
12 | "label": "2. world",
13 | "type": "AlignedLabel"
14 | },
15 | {
16 | "classes": [],
17 | "label": "3. yeah",
18 | "type": "AlignedLabel"
19 | },
20 | {
21 | "classes": [],
22 | "label": "4. sam",
23 | "type": "AlignedLabel"
24 | }
25 | ],
26 | "classes": [
27 | "vertical",
28 | "mdx-block",
29 | "mdx-block-ol"
30 | ],
31 | "label": null,
32 | "type": "Box"
33 | }
34 | ],
35 | "classes": [
36 | "vertical",
37 | "mdx-block",
38 | "mdx-block-div"
39 | ],
40 | "label": null,
41 | "type": "Box"
42 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--quote.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "hello world",
6 | "type": "AlignedLabel"
7 | },
8 | {
9 | "children": [
10 | {
11 | "classes": [],
12 | "label": "quoted",
13 | "type": "AlignedLabel"
14 | },
15 | {
16 | "children": [
17 | {
18 | "classes": [],
19 | "label": "sub quote",
20 | "type": "AlignedLabel"
21 | }
22 | ],
23 | "classes": [
24 | "vertical",
25 | "mdx-block",
26 | "mdx-block-blockquote"
27 | ],
28 | "label": null,
29 | "type": "Box"
30 | }
31 | ],
32 | "classes": [
33 | "vertical",
34 | "mdx-block",
35 | "mdx-block-blockquote"
36 | ],
37 | "label": null,
38 | "type": "Box"
39 | }
40 | ],
41 | "classes": [
42 | "vertical",
43 | "mdx-block",
44 | "mdx-block-div"
45 | ],
46 | "label": null,
47 | "type": "Box"
48 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--super-long.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "hello woah long world",
6 | "type": "AlignedLabel"
7 | }
8 | ],
9 | "classes": [
10 | "vertical",
11 | "mdx-block",
12 | "mdx-block-div"
13 | ],
14 | "label": null,
15 | "type": "Box"
16 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/snapshots/newmarkdown--super.json:
--------------------------------------------------------------------------------
1 | {
2 | "children": [
3 | {
4 | "classes": [],
5 | "label": "hello my world",
6 | "type": "AlignedLabel"
7 | }
8 | ],
9 | "classes": [
10 | "vertical",
11 | "mdx-block",
12 | "mdx-block-div"
13 | ],
14 | "label": null,
15 | "type": "Box"
16 | }
--------------------------------------------------------------------------------
/redditisgtk/tests-data/sublistrows--pm.json:
--------------------------------------------------------------------------------
1 | {"kind": "t4", "data": {"first_message": 690341637, "first_message_name": "t4_bf0f0l", "subreddit": null, "likes": null, "replies": "", "id": "bf34ua", "subject": "re: Blog down", "was_comment": false, "score": 0, "author": "bambambazooka", "num_comments": null, "parent_id": "t4_bf34ky", "subreddit_name_prefixed": null, "new": false, "body": "thanks a lot!", "dest": "samdroid_", "body_html": "", "name": "t4_bf34ua", "created": 1522961304.0, "created_utc": 1522932504.0, "context": "", "distinguished": null}}
2 |
--------------------------------------------------------------------------------
/redditisgtk/tests-data/sublistrows--thumb-from-previews.json:
--------------------------------------------------------------------------------
1 | {"data": {"approved_at_utc": null, "subreddit": "soccer", "selftext": "", "author_fullname": "t2_ta1lp", "saved": false, "mod_reason_title": null, "gilded": 1, "clicked": false, "title": "Red Star Belgrade 2-0 Liverpool - Pavkov 29' (Great Goal)", "link_flair_richtext": [{"e": "text", "t": "Media"}], "subreddit_name_prefixed": "r/soccer", "hidden": false, "pwls": 6, "link_flair_css_class": "media", "downs": 0, "thumbnail_height": 78, "hide_score": false, "name": "t3_9uqwp7", "quarantine": false, "link_flair_text_color": "dark", "author_flair_background_color": "", "subreddit_type": "public", "ups": 7211, "domain": "clippituser.tv", "media_embed": {"content": "", "width": 600, "scrolling": false, "height": 337}, "thumbnail_width": 140, "author_flair_template_id": null, "is_original_content": false, "user_reports": [], "secure_media": {"type": "clippituser.tv", "oembed": {"provider_url": "http://clippit.tv", "description": "UEFA Champions League Soccer: Red Star Belgrade vs. Liverpool | TNT", "title": "UEFA Champions League Soccer: Red Star Belgrade vs. Liverpool | TNT", "thumbnail_width": 640, "height": 337, "width": 600, "html": "", "version": "1.0", "provider_name": "Clippit", "thumbnail_url": "https://clips.clippit.tv/plyeng/thumbnail_share.jpg", "type": "video", "thumbnail_height": 360}}, "is_reddit_media_domain": false, "is_meta": false, "category": null, "secure_media_embed": {"content": "", "width": 600, "scrolling": false, "media_domain_url": "https://www.redditmedia.com/mediaembed/9uqwp7", "height": 337}, "link_flair_text": "Media", "can_mod_post": false, "score": 7211, "approved_by": null, "thumbnail": "image", "edited": false, "author_flair_css_class": "s1 7 team-7 country-usa", "author_flair_richtext": [{"e": "text", "t": "United States"}], "gildings": {"gid_1": 1, "gid_2": 1, "gid_3": 0}, "post_hint": "rich:video", "content_categories": null, "is_self": false, "mod_note": null, "created": 1541557567.0, "link_flair_type": "richtext", "wls": 6, "banned_by": null, "author_flair_type": "richtext", "contest_mode": false, "selftext_html": null, "likes": null, "suggested_sort": null, "banned_at_utc": null, "view_count": null, "archived": false, "no_follow": false, "is_crosspostable": true, "pinned": false, "over_18": false, "preview": {"images": [{"source": {"url": "https://external-preview.redd.it/AxVEZvB8AZsAPFSlrGM3HGDCss0bxYEbNu89NmgUdTg.jpg?auto=webp&s=40dfcc7795f611acf86e8963dc1b3fef2412ca6a", "width": 640, "height": 360}, "resolutions": [{"url": "https://external-preview.redd.it/AxVEZvB8AZsAPFSlrGM3HGDCss0bxYEbNu89NmgUdTg.jpg?width=108&crop=smart&auto=webp&s=a906c3a7241def971591ded4d7f9ff9abe6b050c", "width": 108, "height": 60}, {"url": "https://external-preview.redd.it/AxVEZvB8AZsAPFSlrGM3HGDCss0bxYEbNu89NmgUdTg.jpg?width=216&crop=smart&auto=webp&s=9a957e0d1d877667ec90f74e8ebf3669503357b2", "width": 216, "height": 121}, {"url": "https://external-preview.redd.it/AxVEZvB8AZsAPFSlrGM3HGDCss0bxYEbNu89NmgUdTg.jpg?width=320&crop=smart&auto=webp&s=faf1caaff25518035287d11ab33293ebe4848ad5", "width": 320, "height": 180}, {"url": "https://external-preview.redd.it/AxVEZvB8AZsAPFSlrGM3HGDCss0bxYEbNu89NmgUdTg.jpg?width=640&crop=smart&auto=webp&s=f22637b21961bbf3a265f7b9f8971e11409789c8", "width": 640, "height": 360}], "variants": {}, "id": "GIdzayMU86gAtFQOUfHX8nW06AkjcKoORfsKqIYWX1E"}], "enabled": false}, "media_only": false, "link_flair_template_id": null, "can_gild": true, "spoiler": false, "locked": false, "author_flair_text": "United States", "visited": false, "num_reports": null, "distinguished": null, "subreddit_id": "t5_2qi58", "mod_reason_by": null, "removal_reason": null, "link_flair_background_color": "", "id": "9uqwp7", "is_robot_indexable": true, "report_reasons": null, "author": "HerbalDreamin", "num_crossposts": 0, "num_comments": 597, "send_replies": true, "whitelist_status": "all_ads", "mod_reports": [], "author_patreon_flair": false, "author_flair_text_color": "dark", "permalink": "/r/soccer/comments/9uqwp7/red_star_belgrade_20_liverpool_pavkov_29_great/", "parent_whitelist_status": "all_ads", "stickied": false, "url": "https://www.clippituser.tv/c/plyeng", "subreddit_subscribers": 1219547, "created_utc": 1541528767.0, "media": {"type": "clippituser.tv", "oembed": {"provider_url": "http://clippit.tv", "description": "UEFA Champions League Soccer: Red Star Belgrade vs. Liverpool | TNT", "title": "UEFA Champions League Soccer: Red Star Belgrade vs. Liverpool | TNT", "thumbnail_width": 640, "height": 337, "width": 600, "html": "", "version": "1.0", "provider_name": "Clippit", "thumbnail_url": "https://clips.clippit.tv/plyeng/thumbnail_share.jpg", "type": "video", "thumbnail_height": 360}}, "is_video": false}}
2 |
3 |
--------------------------------------------------------------------------------
/redditisgtk/tests-data/submit--good-response.json:
--------------------------------------------------------------------------------
1 | {"json": {"errors": [], "data": {"url": "https://www.reddit.com/r/test/comments/9teb69/test/", "drafts_count": 0, "id": "9teb69", "name": "t3_9teb69"}}}
2 |
--------------------------------------------------------------------------------
/redditisgtk/tests-data/submit--ratelimit-response.json:
--------------------------------------------------------------------------------
1 | {"json": {"ratelimit": 510.691727, "errors": [["RATELIMIT", "you are doing that too much. try again in 8 minutes.", "ratelimit"]]}}
--------------------------------------------------------------------------------
/redditisgtk/webviews.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Sam Parkinson
2 | #
3 | # This file is part of Something for Reddit.
4 | #
5 | # Something for Reddit 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 | # Something for Reddit 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 Something for Reddit. If not, see .
17 |
18 |
19 | import subprocess
20 | from gi.repository import Gtk
21 | from gi.repository import WebKit2
22 |
23 |
24 | def open_uri_external(uri: str):
25 | '''
26 | Open the given uri in an external browser
27 | '''
28 | subprocess.call(['xdg-open', uri])
29 |
30 |
31 | class FullscreenableWebview(WebKit2.WebView):
32 |
33 | def do_enter_fullscreen(self):
34 | self._old_parent = self.get_parent()
35 | self._old_parent.remove(self)
36 | self._old_parent.get_toplevel().hide()
37 |
38 | self._fullscreen = Gtk.Window()
39 | self._fullscreen.add(self)
40 | self._fullscreen.show()
41 |
42 | def do_leave_fullscreen(self):
43 | self._fullscreen.remove(self)
44 | self._old_parent.add(self)
45 | self._old_parent.get_toplevel().show()
46 | del self._old_parent
47 |
48 | self._fullscreen.destroy()
49 | del self._fullscreen
50 |
51 | _load_when_visible = None
52 | def load_when_visible(self, uri):
53 | self._load_when_visible = uri
54 |
55 | def do_map(self):
56 | WebKit2.WebView.do_map(self)
57 |
58 | if self._load_when_visible is not None:
59 | self.load_uri(self._load_when_visible)
60 | self._load_when_visible = None
61 |
62 |
63 | class ProgressContainer(Gtk.Overlay):
64 | '''
65 | Overlays a progress bar on a webview passed to the constructor
66 | '''
67 |
68 | def __init__(self, webview):
69 | Gtk.Overlay.__init__(self)
70 | self._webview = webview
71 | self.add(self._webview)
72 | self._webview.show()
73 |
74 | self._progress = Gtk.ProgressBar(halign=Gtk.Align.FILL,
75 | valign=Gtk.Align.START)
76 | self.add_overlay(self._progress)
77 |
78 | self._webview.connect('notify::estimated-load-progress',
79 | self.__notify_progress_cb)
80 |
81 | def __notify_progress_cb(self, webview, pspec):
82 | progress = webview.props.estimated_load_progress
83 | if progress == 1.0:
84 | self._progress.hide()
85 | else:
86 | self._progress.show()
87 | self._progress.set_fraction(progress)
88 |
89 |
90 | class WebviewToolbar(Gtk.Bin):
91 |
92 | def __init__(self, webview):
93 | Gtk.Bin.__init__(self)
94 | self._webview = webview
95 | self._b = Gtk.Builder.new_from_resource(
96 | '/today/sam/reddit-is-gtk/webview-toolbar.ui')
97 | self.add(self._b.get_object('toolbar'))
98 | self._b.get_object('toolbar').show_all()
99 |
100 | self._b.get_object('back').connect(
101 | 'clicked', self.__clicked_cb, webview.go_back)
102 | self._b.get_object('forward').connect(
103 | 'clicked', self.__clicked_cb, webview.go_forward)
104 | self._b.get_object('external').connect('clicked', self.__external_cb)
105 |
106 | webview.connect('load-changed', self.__load_changed_cb)
107 |
108 | def __load_changed_cb(self, webview, load_event):
109 | self._b.get_object('back').props.sensitive = \
110 | self._webview.can_go_back()
111 | self._b.get_object('forward').props.sensitive = \
112 | self._webview.can_go_forward()
113 |
114 | def __external_cb(self, button):
115 | uri = self._webview.props.uri
116 | open_uri_external(uri)
117 |
118 | def __clicked_cb(self, button, func):
119 | func()
120 |
--------------------------------------------------------------------------------
/screenshots/0.2.1-askreddit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/screenshots/0.2.1-askreddit.png
--------------------------------------------------------------------------------
/screenshots/0.2.1-dankmemes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/screenshots/0.2.1-dankmemes.png
--------------------------------------------------------------------------------
/screenshots/0.2.1-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samdroid-apps/something-for-reddit/071733a9e6050d0d171e6620c189b7860af56bab/screenshots/0.2.1-dark.png
--------------------------------------------------------------------------------
/today.sam.reddit-is-gtk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app-id": "today.sam.reddit-is-gtk",
3 | "runtime": "org.gnome.Platform",
4 | "runtime-version": "3.28",
5 | "sdk": "org.gnome.Sdk",
6 | "command": "reddit-is-gtk",
7 |
8 | "finish-args": [
9 | /* Display server */
10 | "--share=ipc",
11 | "--socket=x11",
12 | "--socket=wayland",
13 |
14 | /* DRI for WK2 rendering */
15 | "--device=dri",
16 |
17 | /* Access to Reddit */
18 | "--share=network",
19 |
20 | /* DConf for some configuration storage */
21 | "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro",
22 | "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf",
23 |
24 | /* Audio for WK2 */
25 | "--socket=pulseaudio"
26 | ],
27 | "cleanup": [
28 | "/include",
29 | "/lib/pkgconfig",
30 | "/share/pkgconfig",
31 | "/share/aclocal",
32 | "/man",
33 | "/share/man",
34 | "/share/gtk-doc",
35 | "/share/vala",
36 | "*.la",
37 | "*.a"
38 | ],
39 |
40 | "rename-desktop-file": "reddit-is-gtk.desktop",
41 |
42 | "modules": [
43 | {
44 | "name": "pipdeps",
45 | "buildsystem": "simple",
46 |
47 | "build-commands": [
48 | "pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} markdown arrow"
49 | ],
50 | "sources": [
51 | {
52 | "type": "file",
53 | "url": "https://files.pythonhosted.org/packages/3c/52/7bae9e99a7a4be6af4a713fe9b692777e6468d28991c54c273dfb6ec9fb2/Markdown-3.0.1.tar.gz",
54 | "sha256": "d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c"
55 | },
56 | {
57 | "type": "file",
58 | "url": "https://files.pythonhosted.org/packages/e0/86/4eb5228a43042e9a80fe8c84093a8a36f5db34a3767ebd5e1e7729864e7b/arrow-0.12.1.tar.gz",
59 | "sha256": "a558d3b7b6ce7ffc74206a86c147052de23d3d4ef0e17c210dd478c53575c4cd"
60 | },
61 | {
62 | "type": "file",
63 | "url": "https://files.pythonhosted.org/packages/0e/01/68747933e8d12263d41ce08119620d9a7e5eb72c876a3442257f74490da0/python-dateutil-2.7.5.tar.gz",
64 | "sha256": "88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
65 | },
66 | {
67 | "type": "file",
68 | "url": "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six-1.11.0.tar.gz",
69 | "sha256": "70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"
70 | }
71 | ],
72 |
73 | "modules": [
74 | {
75 | "name": "setuptools_scm",
76 | "buildsystem": "simple",
77 | "build-commands": [
78 | "pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} setuptools_scm"
79 | ],
80 | "sources": [
81 | {
82 | "type": "file",
83 | "url": "https://files.pythonhosted.org/packages/09/b4/d148a70543b42ff3d81d57381f33104f32b91f970ad7873f463e75bf7453/setuptools_scm-3.1.0.tar.gz",
84 | "sha256": "1191f2a136b5e86f7ca8ab00a97ef7aef997131f1f6d4971be69a1ef387d8b40"
85 | }
86 | ]
87 | }
88 | ]
89 | },
90 | {
91 | "name": "sassc",
92 | "config-opts": ["--with-libsass=/usr/share/runtime/share/themes/Yaru-dark/gtk-3.0"],
93 | "cleanup": ["*"],
94 | "sources": [
95 | {
96 | "type": "archive",
97 | "url": "https://github.com/sass/sassc/archive/3.5.0.tar.gz",
98 | "sha256": "26f54e31924b83dd706bc77df5f8f5553a84d51365f0e3c566df8de027918042"
99 | },
100 | {
101 | "type": "script",
102 | "dest-filename": "autogen.sh",
103 | "commands": ["autoreconf -si"]
104 | }
105 | ],
106 | "modules": [
107 | {
108 | "name": "libsass",
109 | "cleanup": ["*"],
110 | "sources": [
111 | {
112 | "type": "archive",
113 | "url": "https://github.com/sass/libsass/archive/3.5.4.tar.gz",
114 | "sha256": "5f61cbcddaf8e6ef7a725fcfa5d05297becd7843960f245197ebb655ff868770"
115 | },
116 | {
117 | "type": "script",
118 | "dest-filename": "autogen.sh",
119 | "commands": ["autoreconf -si"]
120 | }
121 | ]
122 | }
123 | ]
124 | },
125 | {
126 | "name": "reddit-is-gtk",
127 | "buildsystem": "autotools",
128 | "sources": [
129 | {
130 | "type": "git",
131 | "url": "https://github.com/samdroid-apps/something-for-reddit"
132 | }
133 | ]
134 | }
135 | ]
136 | }
137 |
--------------------------------------------------------------------------------