Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Webview and Reader mode
-
@mikael I had seen this depreciation and I had searched the new way but not found easily then, just to let it working, I used the depreciated way. But, you're right, I should modernize it.
-
@ihf as advised by @mikael , replace depreciated
#SFSafariViewController = ObjCClass('SFSafariViewController').alloc().initWithURL_entersReaderIfAvailable_(url,True)
by
SFSafariViewControllerConfiguration = ObjCClass('SFSafariViewControllerConfiguration').alloc().init() SFSafariViewControllerConfiguration.setEntersReaderIfAvailable_(True) SFSafariViewController = ObjCClass('SFSafariViewController').alloc().initWithURL_configuration_(url,SFSafariViewControllerConfiguration)
-
I have packaged cvp's solution into a module, which works like a charm, thanks a lot cvp ! (I use it for a newsreader).
I have come across two limitations, though:
- reader_view() cannot be used if 'uiview' has a NavigationView, or if any view presented below it has a NavigationView (even if the view has no relationship with 'uiview' - very strange)
- No resizing is performed when the reader view changes orientation
I am not very proficient with objc, would anyone know how to improve this ?
""" Modal view which displays a web page in reader mode. Based on cvp's code: https://forum.omz-software.com/topic/5392/webview-and-reader-mode/18 When run as a script, the module shows a small demo application. Implementation note: Transmitting the callback function from reader_view() to safariView- ControllerDidFinish_() turns out to be trickier than expected, especially when combining reader_view.py with app_single_launch.py. In this case, when the user launches the same app a second time, a new 'safari_view_controler' instance seems to be created automagically. We therefore cannot store the callback function as one of the 'safari_view_ controler's properties, since the instance created by reader_view() is silently replaced by a new instance when the app is relaunched, and ControllerDidFinish_() will receive the new instance, not the old one. In the end, the simplest solution works, i.e. storing the callback in a global. This does of course come with theoretical limitations: an applica- tion should not make a second call to reader_view() until the reader view displayed by the first call has been closed. This should not be too much of an issue, as I really cannot imagine a practical use case for doing this. I have included a simple protection against this edge case anyway. Revision history: 6-Fev-2019 TPO - Initial release 28-Fev-2019 TPO - Added the 'enter_reader_mode' argument to reader_view() 7-Mar-2019 TPO - reader_view() now works when used with app_single_launch.py """ from typing import Callable, Optional from objc_util import create_objc_class, nsurl, ObjCClass, ObjCInstance import ui __all__ = [ 'reader_view', ] Callback = Callable[[], None] __CALLBACK: Optional[Callback] __READER_VIEW_DISPLAYED = False def safariViewControllerDidFinish_(_self, _cmd, _controller): global __CALLBACK, __READER_VIEW_DISPLAYED if __CALLBACK: __CALLBACK() __READER_VIEW_DISPLAYED = False def reader_view(url: str, uiview: ui.View, callback: Optional[Callback] = None, enter_reader_mode: bool = True) -> None: """ Modal view which displays a web page in reader mode. Arguments: - url: of the web page to be displayed. The only URL schemes allowed are http and https. Any other URL scheme will raise an Objective-C exception and cause Pythonista to be terminated. - uiview: ui.View instance on which the web view is to be displayed. Note that reader_view() does not work if this is an instance of ui.Navigation- View. - callback: function of no argument, which is called when the user closes the view. - enter_reader_mode: if true and if reader mode is available, it will be entered automatically; if false, the user will have to tap the reader mode icon to enter reader mode (when available). """ global __CALLBACK, __READER_VIEW_DISPLAYED if __READER_VIEW_DISPLAYED: raise ValueError("reader_view() already in use") __READER_VIEW_DISPLAYED = True __CALLBACK = callback MySFSafariViewControllerDelegate = create_objc_class( 'MySFSafariViewControllerDelegate', methods=[safariViewControllerDidFinish_], protocols=['SFSafariViewControllerDelegate']) delegate = MySFSafariViewControllerDelegate.alloc().init() safari_view_controler_conf = ( ObjCClass('SFSafariViewControllerConfiguration').alloc().init()) safari_view_controler_conf.setEntersReaderIfAvailable_(enter_reader_mode) safari_view_controler = ObjCClass('SFSafariViewController').alloc() \ .initWithURL_configuration_(nsurl(url), safari_view_controler_conf) safari_view_controler.delegate = delegate safari_view_controler.setModalPresentationStyle_(3) # 3 = currentContext vc = ObjCClass('SUIViewController').viewControllerForView_(ObjCInstance(uiview)) vc.presentViewController_animated_completion_(safari_view_controler, True, None) if __name__ == '__main__': from datetime import datetime def done(): print("After user has closed reader view", datetime.now()) def test(sender): print("Before call to reader_view()", datetime.now()) reader_view(url=url, uiview=view, callback=done) url = 'http://www.nytimes.com/2019/01/29/climate/global-warming-extreme-weather.html' view = ui.View( flex='WH', background_color='white', name='reader_view demo', right_button_items=[ui.ButtonItem(title='test', action=test)]) view.present()
-
@TPO, this I can help you with:
safari_view_controler.view().autoresizingMask = 2 + 16 # WH
-
In addition to adopting cvp's SFSafariView solution, I have been experimenting with JonB's initial suggestion of using Safari's JS code for the reader mode.
The reason for this is that SFSafariView is a black box which does not provide a way of getting / setting the position in the page. Being able to do this would allow a newsreader app to remember where the user stopped reading an article, and later re-open the article at this very position. This would be convenient for long articles which cannot be read in one sitting.
If we had a way a generating a Safari-reader-mode-like filtered version of the web page, we could display it in a WebView and then get / set the position (as demonstrated by MarkdownView, https://github.com/mikaelho/pythonista-markdownview) to achieve this result.
I have digged a bit into this, have found a more recent and more complete version of Safari's reader mode source code (both JS and CSS), and have had some success with it:
- The web page is cleaned of most useless content
- It is displayed in the mode selected (for example : sepia), with the font selected
However:
- Not all useless content is removed, and the result is not as good as what Safari produces
- Margins and spacing between paragraphs are not handled properly
You will find below my test code and pointers to the Safari sources, in case anyone is interested in investigating this further.
Download the following files :
- safari.js : https://github.com/liruqi/Safari/blob/master/safari.js
- article-content.css : https://github.com/andjosh/safari-reader-css/blob/master/article-content.css
- reader-ui.css : https://github.com/andjosh/safari-reader-css/blob/master/reader-ui.css
- theming.css : https://github.com/andjosh/safari-reader-css/blob/master/theming.css
Then run the following in the same directory :
import wkwebview import ui with open('safari.js', 'r', encoding='utf-8') as f: safari = f.read() with open('reader-ui.css', 'r', encoding='utf-8') as f: reader_ui_css = f.read() with open('article-content.css', 'r', encoding='utf-8') as f: article_content_css = f.read() with open('theming.css', 'r', encoding='utf-8') as f: theming_css = f.read() js_script = f''' r = new ReaderArticleFinder(document); document.body.innerHTML = r.articleNode().innerHTML; document.body.classList="system sepia"; var style = document.createElement('style'); style.innerHTML = `{reader_ui_css}\n{article_content_css}\n{theming_css}`; var ref = document.querySelector('script'); ref.parentNode.insertBefore(style, ref); document.getElementById("dynamic-article-content").sheet.insertRule("#article {{ font-size:15 px; line-height:25px; }}"); ''' class Delegate: @ui.in_background def webview_did_finish_load(self, webview): w.eval_js(js_script) w = wkwebview.WKWebView(delegate=Delegate()) w.present() w.add_script(safari) w.load_url('https://www.technologyreview.com/s/612929/wristwatch-heart-monitors-might-save-your-lifeand-change-medicine-too/')
-
@mikael : works like a charm, thanks a lot !
-
@TPO, looks like there is at least a
contentScrollView
attribute. Maybe you could dig deeper and get hold of the scroll position. -
-
@mikael you are quite right, dir() shows that safari_view_controler has a 'contentScrollView ' attribute.
However, safari_view_controler.contentScrollView() returns None, both in reader_view() and in safariViewControllerDidFinish_() :-(
I have gone through the official Apple doc on SFSafariViewController, 'contentScrollView ' is not mentionned anywhere.
-
@TPO said:
presentViewController
I wonder if addChildViewController (along with uiview.objc_instance.addSubview_(vc.view())) might be more appropriate than presentViewController here, if you are trying to essentially add a subview instead of creating a new fullscreen view.