Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Publicaciones sobre open source

APIs de Ubuntu One en Ejemplos (parte 1)

Así que acá va un pe­que­ño tu­to­rial acer­ca de co­mo usar al­gu­nas de esas APIs. Lo hi­ce usan­do Py­thon y Py­Qt por va­rios mo­ti­vo­s:

  • Son ex­­ce­­len­­tes he­­rra­­mien­­tas pa­­ra pro­­­to­­­ti­­pos

  • Tie­­nen ex­­ce­­len­­te so­­­po­r­­te pa­­ra las co­­sas que ne­­ce­­si­­to (DBus, HTTP, OAu­­th)

  • Es lo que sé y me gus­­ta. Lo hi­­ce un do­­­mi­n­­go, no lo pien­­so ha­­cer en PHP y Gtk.

Di­cho eso, no hay na­da es­pe­cí­fi­co de py­thon o de Qt en es­te có­di­go. Don­de ha­go un re­quest HTTP usan­do QtNe­two­rk, po­dés usar lib­soup o lo que fue­re.

Va­ya­mos a los bi­fes en­ton­ce­s. Las pie­zas más im­por­tan­tes de Ubun­tu One, des­de el pun­to de vis­ta de in­fra­es­truc­tu­ra, son Ubun­tu SSO Clien­t, que se en­car­ga de lo­gi­n, re­gis­tra­ció­n, etc, y Syn­c­Dae­mos que ma­ne­ja la sin­cro­ni­za­ción de ar­chi­vo­s.

Pa­ra in­te­rac­tuar con ella­s, en Li­nu­x, ofre­cen in­ter­fa­ces DBus. Así que, por ejem­plo, es­te es un frag­men­to mos­tran­do co­mo ob­te­ner las cre­den­cia­les de Ubun­tu One (es­to nor­mal­men­te se­ría par­te del __i­ni­t__ de un ob­je­to­):

# Get the session bus
bus = dbus.SessionBus()

:
:
:

# Get the credentials proxy and interface
self.creds_proxy = bus.get_object("com.ubuntuone.Credentials",
                        "/credentials",
                        follow_name_owner_changes=True)

# Connect to signals so you get a call when something
# credential-related happens
self.creds_iface = dbus.Interface(self.creds_proxy,
    "com.ubuntuone.CredentialsManagement")
self.creds_proxy.connect_to_signal('CredentialsFound',
    self.creds_found)
self.creds_proxy.connect_to_signal('CredentialsNotFound',
    self.creds_not_found)
self.creds_proxy.connect_to_signal('CredentialsError',
    self.creds_error)

# Call for credentials
self._credentials = None
self.get_credentials()

Tal vez no­tas­te que ge­t_­cre­den­tials no de­vuel­ve las cre­den­cia­le­s. Lo que ha­ce es, le di­ce a Syn­c­Dae­mon que las ob­ten­ga, y en­ton­ce­s, si/­cuan­do apa­re­cen, se emi­te una de esas se­ña­le­s, y uno de los mé­to­dos co­nec­ta­dos se lla­ma. Es­to es­tá bue­no por­que no te­ne­mos que preo­cu­par­nos de que se nos blo­quee la apli­ca­ción mien­tras Syn­c­Dae­mon es­tá bus­can­do las cre­den­cia­le­s.

¿Y qué hay en esos mé­to­do­s? ¡No mu­cho!

def get_credentials(self):
    # Do we have them already? If not, get'em
    if not self._credentials:
        self.creds_proxy.find_credentials()
    # Return what we've got, could be None
    return self._credentials

def creds_found(self, data):
    # Received credentials, save them.
    print "creds_found", data
    self._credentials = data
    # Don't worry about get_quota yet ;-)
    if not self._quota_info:
        self.get_quota()

def creds_not_found(self, data):
    # No credentials, remove old ones.
    print "creds_not_found", data
    self._credentials = None

def creds_error(self, data):
    # No credentials, remove old ones.
    print "creds_error", data
    self._credentials = None

Así que bá­si­ca­men­te, se­l­f._­cre­den­tials con­tie­ne unas cre­den­cia­le­s, o No­ne. Fe­li­ci­ta­cio­nes, ya en­tra­mos a Ubun­tu One.

¡Ha­ga­mos al­go úti­l! ¿Que tal pre­gun­tar cuán­to es­pa­cio li­bre hay en la cuen­ta? Pa­ra eso, no po­de­mos usar las APIs lo­ca­le­s, si no co­nec­tar­nos a los ser­ver­s, que son los que sa­ben si es­tás ex­ce­di­do de quo­ta o no.

El ac­ce­so se con­tro­la via OAu­th, por lo que pa­ra ac­ce­der a esa API ne­ce­si­ta­mos fir­mar nues­tros pe­di­do­s. Aquí se ve co­mo se ha­ce. No es par­ti­cu­lar­men­te ilu­mi­na­do­r, yo no lo es­cri­bí, so­la­men­te lo uso:

def sign_uri(self, uri, parameters=None):
    # Without credentials, return unsigned URL
    if not self._credentials:
        return uri
    if isinstance(uri, unicode):
        uri = bytes(iri2uri(uri))
    print "uri:", uri
    method = "GET"
    credentials = self._credentials
    consumer = oauth.OAuthConsumer(credentials["consumer_key"],
                                   credentials["consumer_secret"])
    token = oauth.OAuthToken(credentials["token"],
                             credentials["token_secret"])
    if not parameters:
        _, _, _, _, query, _ = urlparse(uri)
        parameters = dict(cgi.parse_qsl(query))
    request = oauth.OAuthRequest.from_consumer_and_token(
                                        http_url=uri,
                                        http_method=method,
                                        parameters=parameters,
                                        oauth_consumer=consumer,
                                        token=token)
    sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
    request.sign_request(sig_method, consumer, token)
    print "SIGNED:", repr(request.to_url())
    return request.to_url()

¿Y có­mo pe­di­mos el es­ta­do de quo­ta? Ac­ce­dien­do al pun­to de en­tra­da http­s://o­ne.u­bun­tu.­co­m/a­pi/­quo­ta/ con la au­to­ri­za­ción ade­cua­da, se ob­tie­ne un dic­cio­na­rio JSON con el es­pa­cio to­tal y el usa­do. Acá hay una mues­tra de co­mo ha­cer­lo:

    # This is on __init__
    self.nam = QtNetwork.QNetworkAccessManager(self,
        finished=self.reply_finished)

:
:
:

def get_quota(self):
    """Launch quota info request."""
    uri = self.sign_uri(QUOTA_API)
    url = QtCore.QUrl()
    url.setEncodedUrl(uri)
    self.nam.get(QtNetwork.QNetworkRequest(url))

De nue­vo: ge­t_­quo­ta no de­vuel­ve la quo­ta. Só­lo lan­za un pe­di­do HTTP a los ser­vers de Ubun­tu One, que (e­ven­tual­men­te) res­pon­den con los da­to­s. No que­rés que tu app se que­de ahí tra­ba­da mien­tras tan­to, por eso QNe­two­rkAc­ce­ss­Ma­na­ger va a lla­mar a se­l­f.­re­pl­y_­fi­nis­hed cuan­do ten­ga la res­pues­ta:

def reply_finished(self, reply):
    if unicode(reply.url().path()) == u'/api/quota/':
        # Handle quota responses
        self._quota_info = json.loads(unicode(reply.readAll()))
        print "Got quota: ", self._quota_info
        # Again, don't worry about update_menu yet ;-)
        self.update_menu()

¿Qué más que­re­mo­s? ¿Qué tal no­ti­fi­ca­ción cuan­do cam­bia el sta­tus de Syn­c­Dae­mo­n? Por ejem­plo, cuan­do la sin­cro­ni­za­ción es­tá al día, o cuan­do te des­co­nec­ta. De nue­vo, esas son se­ña­les DBus a las que uno se co­nec­ta en __i­ni­t__:

self.status_proxy = bus.get_object(
    'com.ubuntuone.SyncDaemon', '/status')
self.status_iface = dbus.Interface(self.status_proxy,
    dbus_interface='com.ubuntuone.SyncDaemon.Status')
self.status_iface.connect_to_signal(
    'StatusChanged', self.status_changed)

# Get the status as of right now
self._last_status = self.process_status(
    self.status_proxy.current_status())

Y es­ta es sta­tus_­chan­ge­d:

def status_changed(self, status):
    print "New status:", status
    self._last_status = self.process_status(status)
    self.update_menu()

la fun­ción pro­ce­ss_s­ta­tus is có­di­go abu­rri­do pa­ra con­ver­tir la in­fo de sta­tus de syn­c­dae­mon en una co­sa le­gi­ble co­mo "S­ync is up-­to­-­da­te", así que guar­da­mos eso en se­l­f._­las­t_s­ta­tus y ac­tua­li­za­mos el me­nú.

¿Qué me­nú? ¡Un me­nú con­tex­tual de un QS­ys­te­m­Tra­yI­co­n! Lo que le­ye­ron son las pie­zas prin­ci­pa­les que se ne­ce­si­tan pa­ra crear al­go úti­l: una apli­ca­ción de Sys­te­m­Tray pa­ra Ubun­tu One que se pue­de usar en KDE, XFCE u Open­bo­x. O, si es­tás en uni­ty y te­nés sni-­qt ins­ta­la­do, un app in­di­ca­to­r.

http://ubuntuone.com/7iXTbysoMM9PIUS9Ai4TNn

El in­di­ca­dor en ac­ció­n.

El có­di­go fuen­te del ejem­plo com­ple­to es­tá en mi pro­yec­to u1-­to­ys en laun­ch­pad y és­te es el có­di­go fuen­te com­ple­to (ex­cep­to los ico­nos, ba­jen­se el re­po, me­jo­r)

Vi­nien­do pron­to (es­pe­ro­), más apps de ejem­plo, y co­sas co­pa­das que se pue­den ha­cer con nues­tras APIs.

Creando un foro de la manera fácil (32 líneas)

Aquí es­tán al­gu­nos de los fea­tu­res que quie­ro:

  • Lo­­­gin via twi­­tter / Fa­­ce­­book / Google / Ope­­nID

  • Nú­­me­­ro ili­­mi­­ta­­do de th­­rea­­ds

  • So­­­po­r­­te de like / dis­­like en th­­rea­­ds y en po­s­­ts

  • Ava­­ta­­res

  • HT­­ML en los po­s­­ts

  • Que man­­de mail al usua­­rio si le res­­po­n­­den

  • Fee­­ds RSS pa­­ra los th­­rea­­ds

Se lo pue­de ver en ac­ción en http://­fo­ro­.­ne­t­ma­na­ger­s.­co­m.ar (por un tiem­po li­mi­ta­do ;-)

Y aquí es­tá el có­di­go:

import bottle
import disqusapi as disqus
import json
shortname = 'magicmisteryforum'
api = disqus.DisqusAPI(open("key").read().strip())

@bottle.route('/', method='GET')
def index():
    msg = bottle.request.GET.get('msg', '')
    threads = api.forums.listThreads(forum=shortname, limit=100)
    print threads[0]
    return bottle.template('main.tpl', threads=threads, shortname=shortname, msg=msg)

@bottle.route('/new', method='POST')
def new():
    title = bottle.request.forms.get('title', None)
    if not title:
        bottle.redirect('/?msg=Missing%20Thread%20Name')
        return
    thread = api.threads.create(forum=shortname, title = title)
    thread_id = thread.__dict__['response']['id']
    # Redirecting to /thread/thread_id doesn't work
    # because threads take a few seconds to appear on the listing
    bottle.redirect('/')

@bottle.route('/thread/:id')
def thread(id):
    t = api.threads.details(thread=id)
    return bottle.template('thread.tpl', shortname=shortname, id=id, thread=t.__dict__['response'])

@bottle.route('/static/:path#.+#')
def server_static(path):
    return bottle.static_file(path, root='./static')

app = bottle.app()
app.catchall = False #Now most exceptions are re-raised within bottle.
bottle.run(host='184.82.108.14', port=80, app=app)

Re­quie­re Bo­ttle y la Dis­qus py­thon API

Por su­pues­to que hay un po­qui­to de tem­pla­tes, acá es­tá mai­n.­tpl y th­rea­d.­tpl. Co­mo apes­to pa­ra el HT­M­L, usa Blue­trip CSS y es sen­ci­llo de cus­to­mi­za­r.

POR SU­PUES­TO QUE HA­GO TRAM­PA!

Es­ta co­sa es ape­nas una ca­pa de pin­tu­ra en­ci­ma de Dis­qus! Más un blog sin pos­ts pe­ro con co­men­ta­rios que un fo­ro! Pe­ro­... qué le fal­ta pa­ra ser un fo­ro de ver­da­d? Fun­cio­na, no? Has­ta se po­drían usar ca­te­go­rías de Dis­qus pa­ra crear su­bfo­ro­s...

Te­nien­do to­do en cuen­ta, creo que es un ha­ck bo­ni­to.

Y si es­pe­rás unos día­s, es­to lle­va a otra co­sa que es mu­cho más má­gi­ca...

Có­di­go fuen­te com­ple­to en http://­ma­gi­cfo­ru­m.­google­co­de.­com

En 128 líneas de código entra exactamente ESTE browser.

Por su­pues­to, po­dría ha­cer má­s, pe­ro has­ta yo ten­go mis stan­dar­d­s!

  • No usar ;

  • No usar if whate­ve­r: f()

Sal­vo eso, hi­ce al­gu­nos tru­cos su­cio­s, pe­ro, en es­te mo­men­to, es un bro­w­ser bas­tan­te com­ple­to en 127 lí­neas de có­di­go se­gún sloc­coun­t, así que ya ju­gué su­fi­cien­te y ma­ña­na ten­go tra­ba­jo que ha­ce­r.

Pe­ro an­tes, con­si­de­re­mos co­mo se im­ple­men­ta­ron al­gu­nos fea­tu­res (voy a cor­tar las lí­neas pa­ra que la pá­gi­na que­de ra­zo­na­ble­men­te an­gos­ta), y vea­mos tam­bién las ver­sio­nes "nor­ma­le­s" de lo mis­mo. La ver­sión "nor­ma­l" no es­tá pro­ba­da, avi­sen si es­tá ro­ta ;-)

Es­to noes al­go que de­ba apren­der­se. De he­cho es ca­si un tra­ta­do en co­mo no ha­cer las co­sas. Es el có­di­go me­nos pi­tó­ni­co y me­nos cla­ro que vas a ver es­ta se­ma­na.

Es cor­to, es ex­pre­si­vo, pe­ro es feo feo.

Voy a co­men­tar so­bre es­ta ver­sión.

Soporte deProxy

Un bro­w­ser no es gran co­sa si no se pue­de usar con pro­x­y. Por suer­te el sta­ck de red de Qt tie­ne buen so­por­te de pro­x­y. El chis­te es con­fi­gu­rar­lo.

De Vicenzo soporta proxies HTTP y SOCKS parseando la variable de entorno http_proxy y seteando el proxy a nivel aplicación en Qt:

 proxy_url = QtCore.QUrl(os.environ.get('http_proxy', ''))
 QtNetwork.QNetworkProxy.setApplicationProxy(QtNetwork.QNetworkProxy(\
 QtNetwork.QNetworkProxy.HttpProxy if unicode(proxy_url.scheme()).startswith('http')\
 else QtNetwork.QNetworkProxy.Socks5Proxy, proxy_url.host(),\
 proxy_url.port(), proxy_url.userName(), proxy_url.password())) if\
'http_proxy' in os.environ else None

Co­mo es la ver­sión nor­mal de esa co­sa?

if 'http_proxy' in os.environ:
    proxy_url = QtCore.QUrl(os.environ['http_proxy'])
    if unicode(proxy_url.scheme()).starstswith('http'):
        protocol = QtNetwork.QNetworkProxy.HttpProxy
    else:
        protocol = QtNetwork.QNetworkProxy.Socks5Proxy
    QtNetwork.QNetworkProxy.setApplicationProxy(
        QtNetwork.QNetworkProxy(
            protocol,
            proxy_url.host(),
            proxy_url.port(),
            proxy_url.userName(),
            proxy_url.password()))

Los abu­sos prin­ci­pa­les contra py­thon son el uso del ope­ra­dor ter­na­rio pa­ra ha­cer un if de una lí­nea (y ani­dar­lo) y el lar­go de lí­nea.

Cookies Persistentes

Es­to es ne­ce­sa­rio por­que que­rés per­ma­ne­cer lo­guea­do en los si­tios de una se­sión a otra. Pa­ra es­to, pri­me­ro tu­ve que ha­cer un pe­que­ño me­ca­nis­mo de per­sis­ten­cia, y guar­da­r/­leer los cookies de ahí.

Acá está como hice la persistencia (settings is una instancia de QSettings global):

def put(self, key, value):
    "Persist an object somewhere under a given key"
    settings.setValue(key, json.dumps(value))
    settings.sync()

def get(self, key, default=None):
    "Get the object stored under 'key' in persistent storage, or the default value"
    v = settings.value(key)
    return json.loads(unicode(v.toString())) if v.isValid() else default

No es có­di­go muy ra­ro, sal­vo por usar el ope­ra­dor ter­na­rio al fi­na­l. El uso de json me ase­gu­ra que mien­tras me­ta co­sas ra­zo­na­ble­s, voy a ob­te­ner lo mis­mo de vuel­ta, con el mis­mo ti­po, sin ne­ce­si­dad de con­ver­tir­lo o lla­mar mé­to­dos es­pe­cia­le­s.

¿Entonces, como guardo/leo los cookies? Primero se necesita acceder el "cookie jar". No encontré si hay uno global o por view, así que creé un QNetworkCookieJar en la línea 24 y la asigno a cada página en la línea 107.

# Save the cookies, in the window's closeEvent
self.put("cookiejar", [str(c.toRawForm()) for c in self.cookies.allCookies()])

# Restore the cookies, in the window's __init__
self.cookies.setAllCookies([QtNetwork.QNetworkCookie.parseCookies(c)[0]\
for c in self.get("cookiejar", [])])

Confieso mi crimen de usar comprensiones de listas cuando la herramienta correcta era un for.

Uso el mis­mo tru­co al res­tau­rar los ta­bs abier­to­s, con el mo­co agre­ga­do de usar una com­pren­sión de lis­ta y des­car­tar el re­sul­ta­do:

# get("tabs") is a list of URLs
[self.addTab(QtCore.QUrl(u)) for u in self.get("tabs", [])]

Propiedades y Señales al crear un objeto

Es­te fea­tu­re es­tá en ver­sio­nes re­cien­tes de Py­Q­t: si pa­sás nom­bres de pro­pie­da­des co­mo ar­gu­men­tos con nom­bre, se les asig­na el va­lo­r. Si pa­sás una se­ñal co­mo ar­gu­men­to con nom­bre, se co­nec­tan al va­lo­r.

Es un fea­tu­re ex­ce­len­te, que te ayu­da a crear có­di­go cla­ro, lo­cal y con­ci­so, y me en­can­ta te­ner­lo. Pe­ro si te que­rés ir a la ban­qui­na, es man­da­da a ha­ce­r.

Es­to es­tá por to­dos la­dos en De Vi­cen­zo, és­te es só­lo un ejem­plo (sí, es una so­la lí­nea):

QtWebKit.QWebView.__init__(self, loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None, loadFinished=self.pbar.hide, loadStarted=lambda:\
self.pbar.show() if self.amCurrent() else None, titleChanged=lambda\
t: container.tabs.setTabText(container.tabs.indexOf(self), t) or\
(container.setWindowTitle(t) if self.amCurrent() else None))

Por adon­de em­pie­zo­...

Hay expresiones lambda usadas para definir los callbacks en el lugar en vez de conectarse con una función o método "de verdad".

Hya lamb­das con el ope­ra­dor ter­na­rio:

loadStarted=lambda:\
    self.pbar.show() if self.amCurrent() else None

Hay lambdas que usan or o una tupla para engañar al intérprete y que haga más de una cosa en un solo lambda!

loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None

No voy ni a in­ten­tar des­en­re­dar es­to con fi­nes edu­ca­ti­vo­s, pe­ro di­ga­mos que esa lí­nea con­tie­ne co­sas que de­be­rían ser 3 mé­to­dos se­pa­ra­do­s, y de­be­ría es­tar re­par­ti­da en 6 lí­neas o ma­s.

Download Manager

Lla­mar­lo un ma­na­ger es exa­ge­rar por­que no se pue­de pa­rar una des­car­ga des­pués que em­pie­za, pe­ro bue­no, te de­ja ba­jar co­sas y se­guir bro­w­sean­do, y te da un re­por­te de pro­gre­so!

Primero, en la línea 16 creé un diccionario bars para llevar registro de los downloads.

Des­pué­s, te­nía que de­le­gar el con­te­ni­do no so­por­ta­do al mé­to­do in­di­ca­do, y eso se ha­ce en las lí­neas 108 and 109

Básicamente, con eso cada vez que hacés click en algo que WebKit no puede manejar, se llama al método fetch con el pedido de red como argumento.

def fetch(self, reply):
    destination = QtGui.QFileDialog.getSaveFileName(self, \
        "Save File", os.path.expanduser(os.path.join('~',\
            unicode(reply.url().path()).split('/')[-1])))
    if destination:
        bar = QtGui.QProgressBar(format='%p% - ' +
            os.path.basename(unicode(destination)))
        self.statusBar().addPermanentWidget(bar)
        reply.downloadProgress.connect(self.progress)
        reply.finished.connect(self.finished)
        self.bars[unicode(reply.url().toString())] = [bar, reply,\
            unicode(destination)]

No hay mu­cho golf acá sal­vo las lí­neas lar­ga­s, pe­ro una vez que me­tés en­ters es la ma­ne­ra ob­via de ha­cer­lo:

  • Pe­­dí un no­m­­bre de ar­­chi­­vo

  • Creás un pro­­­gress­­ba­­r, lo po­­­nés en el sta­­tus­­ba­­r, y lo co­­­ne­c­­tas a las se­­ña­­les de pro­­­gre­­so de la des­­ca­r­­ga.

Entonces, por supuesto, está el slot progress que actualiza la barra:

progress = lambda self, received, total:\
    self.bars[unicode(self.sender().url().toString())][0]\
    .setValue(100. * received / total)

Sí, de­fi­ní un mé­to­do co­mo lamb­da pa­ra aho­rrar una lí­nea. [fa­ce­pal­m]

Y elslot finished para cuando termina el download:

def finished(self):
    reply = self.sender()
    url = unicode(reply.url().toString())
    bar, _, fname = self.bars[url]
    redirURL = unicode(reply.attribute(QtNetwork.QNetworkRequest.\
        RedirectionTargetAttribute).toString())
    del self.bars[url]
    bar.deleteLater()
    if redirURL and redirURL != url:
        return self.fetch(redirURL, fname)
    with open(fname, 'wb') as f:
        f.write(str(reply.readAll()))

has­ta so­por­ta re­di­rec­cio­nes co­rrec­ta­men­te! Más allá d eso, na­da más es­con­de la ba­rra, guar­da los da­to­s, fin del cuen­ti­to. La lí­nea lar­ga ni si­quie­ra es mi cul­pa!

Hay un pro­ble­ma en que el ar­chi­vo en­te­ro se man­tie­ne en me­mo­ria has­ta el fin de la des­car­ga. Si te ba­jás un DV­D, te va a do­le­r.

Usar el with ahorra una línea y no pierde un file handle, comparado con las alternativas.

Impresión

De nue­vo Qt me sal­va las pa­pa­s, por­que ha­cer es­to a ma­no de­be ser di­fí­ci­l. Sin em­bar­go, re­sul­ta que el so­por­te de im­pre­sió­n... es­tá he­cho. Qt, es­pe­cial­men­te usa­do vía Py­Qt es tan com­ple­to!

self.previewer = QtGui.QPrintPreviewDialog(\
    paintRequested=self.print_)
self.do_print = QtGui.QShortcut("Ctrl+p",\
    self, activated=self.previewer.exec_)

No ne­ce­si­té na­da de gol­f. Eso es exac­ta­men­te el có­di­go que se ne­ce­si­ta, y es la ma­ne­ra re­co­men­da­da de en­gan­char "C­tr­l+­p" con la im­pre­sión de la pá­gi­na.

Otros Trucos

No hay otros tru­co­s. To­do lo que que­da es crear wi­dge­ts, co­nec­tar unas co­sas con otra­s, y dis­fru­tar la in­creí­ble ex­pe­rien­ce de pro­gra­mar Py­Q­t, don­de po­dés es­cri­bir un web bro­w­ser en­te­ro (s­al­vo el mo­to­r) en 127 lí­neas de có­di­go.

De Vicenzo: un mini browser más copado

Si no que­rés leer eso de nue­vo, la idea es ver cuán­to có­di­go fal­ta pa­ra con­ver­tir el mo­tor We­bKit de Qt en un bro­w­ser "en se­rio­".

Pa­ra ello, me pu­se una me­ta com­ple­ta­men­te ar­bi­tra­ria de 128 lí­neas de có­di­go. En es­te mo­men­to lo de­cla­ro fea­tu­re-­com­ple­te (pe­ro bu­gg­y).

Los nue­vos fea­tu­res so­n:

  • Ta­­bbed bro­­w­­sing (se pue­­de agre­­ga­­r/s­a­­car ta­bs)

  • Book­­ma­­rks (se pue­­den agre­­ga­­r/s­a­­car y ele­­gir de una lis­­ta)

Es­to es lo que ya fun­cio­na­ba:

  • Zoom in (C­­tr­­l++)

  • Zoom out (C­­tr­­l+-)

  • Re­set Zoom (C­­tr­­l+=)

  • Bus­­car (C­­tr­­l+­­F)

  • Es­­co­n­­der bús­­que­­da (Es­­c)

  • Bo­­­to­­­nes de atrá­s/a­­de­­lan­­te y re­­ca­r­­gar

  • En­­tra­­da de URL que coi­n­­ci­­de con la pá­­gi­­na + au­­to­­­co­m­­ple­­ta­­do des­­de la his­­to­­­ria + arre­­gla la URL pues­­ta a ma­no (a­­gre­­ga http://, esas co­­sas)

  • Plu­­gins (i­n­­cluí­­do fla­s­h, que hay que ba­­jar apa­r­­te ;-)

  • El tí­­tu­­lo de la ven­­ta­­na mues­­tra el tí­­tu­­lo de la pá­­gi­­na (sin pro­­­pa­­gan­­da del bro­­w­se­­r)

  • Ba­­rra de pro­­­gre­­so pa­­ra la ca­r­­ga de la pá­­gi­­na

  • Ba­­rra de es­­ta­­do que mues­­tra el des­­tino de los li­nks cuan­­do pa­sas el mou­­se

  • To­­­ma una URL en la lí­­nea de co­­­man­­do (o abre http://­­p­­y­­tho­­n.org

  • Mu­l­­ti­­pla­­ta­­fo­r­­ma (fun­­cio­­­na do­n­­de fun­­cio­­­na QtWe­­bKi­­t)

Y cuan­to có­di­go es eso? 87 LI­NEAS.

O si pre­fe­rís la ver­sión que cum­ple con la PE­P8: 115 LI­NEAS.

Me ata­jo an­tes que al­guien lo di­ga: sí, el mo­tor de ren­de­ring y el toolkit son enor­mes. Lo que es­cri­bí es el "ch­ro­me" al­re­de­dor de eso, igual que ha­cen Aro­ra, Rekon­q, Ga­leo­n, Epi­phan­y, y mu­chos otros bro­w­ser­s.

Es un ch­ro­me sim­ple y mi­ni­ma­lis­ta, pe­ro fun­cio­na bas­tan­te bien, creo yo.

Aquí es­tá el de­mo (bu­gg­y):

Mas o me­nos ha­ce lo que es­pe­ra­ba que se pue­die­ra lo­gra­r, pe­ro le fal­tan arre­glo­s.

Pa­ra ver el có­di­go, va­yan a su ho­me pa­ge: http://­de­vi­cen­zo­.­google­co­de.­com


Contents © 2000-2023 Roberto Alsina