Wednesday, May 21, 2014
Quick tip: CSS for Hanging indentation
Sometimes you need to achieve a fancy effect on the first line of a paragraph, so that just the first line is indented to the left and the other lines be displayed a little bit to the right. Even, I discovered that it has a name 'Hanging Indent':
This is the first line of a very very very very very
very very very very very very very very very very
long paragraph. And there is more than one sentence.
In fact we could reach almost a dozen.
Just apply a combination of a negative text-indent and padding-left (or margin-left). Ex:
p {
text-align: left;
text-indent: -25px;
padding-left: 25px;
}
There is a 'first-letter' property in css that gives you more customization @see this
Monday, May 19, 2014
Quick tip: optimize images from command line
I use this trick for quickly optimize images. Note, I use ubuntu:
just install these tools
sudo apt-get install optipng jpegoptim
and now you can do (beware: input is overwritten)
optipng file.png
jpegoptim file.jpg
both tools work in the same way
jpegoptim [ options ] filenames
optipng [options] files
to convert between formats, you can use imageMagick
sudo apt-get install imagemagick
and now
convert input.png output.jpg
convert input.jpg output.png
Profit!
just install these tools
sudo apt-get install optipng jpegoptim
and now you can do (beware: input is overwritten)
optipng file.png
jpegoptim file.jpg
both tools work in the same way
jpegoptim [ options ] filenames
optipng [options] files
to convert between formats, you can use imageMagick
sudo apt-get install imagemagick
and now
convert input.png output.jpg
convert input.jpg output.png
Profit!
Sunday, March 23, 2014
Handy enhancement for ruby String: Defaults to default text
In my code , I repeat a lot this pattern: if the string is blank / empty, return a default text, otherwise return the original text.
final_text = text.blank? ? "default text" : text content_tag :h1, text.blank? ? "default text" : text, :class => 'mega'
I discovered this little gem in Stackoverflow (I lost the original source), you only need to reopen the String class, in an intializer, or decorator.
class String
def defaults_to(what)
self.strip!
self.blank? ? what : self
end
end
and, now you can use it in a readable form
text.defaults_to("default text")
final_text = text.blank? ? "default text" : text content_tag :h1, text.blank? ? "default text" : text, :class => 'mega'
I discovered this little gem in Stackoverflow (I lost the original source), you only need to reopen the String class, in an intializer, or decorator.
class String
def defaults_to(what)
self.strip!
self.blank? ? what : self
end
end
and, now you can use it in a readable form
text.defaults_to("default text")
Saturday, March 1, 2014
Concerns in Rails (or how to reuse code)
I have been doing some experiments with rails Concerns . While they have been widely publicited for Rails 4, you can use them in Rails 3 as well.
They are like the ruby standard include / extend pattern, but taking care of dependencies. In any case it would be easy to go back to the 'raw' include / exclude game.
This is an example of how to use them.
require 'active_support/concern'
module Concerns
module DummyConstants
end
module Dummy
extend ActiveSupport::Concern
#it seems that things declared here are shared between all inclusions (saving memeory)
SEXY_REGEXP = /SEXY_REGEXP/i
MEM_HOGGING = Array.new(1024 * 1024)
MEMBERSHIP_STATUSES = %w(accepted invited requested rejected_by_group rejected_group)
included do
#The included block will be triggered at inclusion time
before_create :stuff_on_creation
attr_accessor :accesor_for_the_instance
class << self
attr_accessor :accesor_for_the_class
end
end
module ClassMethods
#Methods in ClassMethods will get added class methods
def dummy?
puts "\nCLASS dummy? called\n"
true
end
end
#these methods are added as instance methods
def dummy?
puts "\nINSTANCE dummy? called\n"
true
end
def stuff_on_creation
puts "\ncreation called\n"
end
end
end
Just create a file under model/concerns/dummy.rb and include it
User.send :include, Concerns::Dummy
or
class User
include Concerns::Dummy end
Note that I repeat the namespace Concerns, but in other examples found in internet people dont. Rails 4 has included models/concerns and controllers/concerns in the autoload paths and in Rails 4 it would work without the namespace. In Rails 3, you need to add the namespace OR using some trick.
I find easier to understand if I include the 'Concern', tough.
Profit!
They are like the ruby standard include / extend pattern, but taking care of dependencies. In any case it would be easy to go back to the 'raw' include / exclude game.
This is an example of how to use them.
require 'active_support/concern'
module Concerns
module DummyConstants
end
module Dummy
extend ActiveSupport::Concern
#it seems that things declared here are shared between all inclusions (saving memeory)
SEXY_REGEXP = /SEXY_REGEXP/i
MEM_HOGGING = Array.new(1024 * 1024)
MEMBERSHIP_STATUSES = %w(accepted invited requested rejected_by_group rejected_group)
included do
#The included block will be triggered at inclusion time
before_create :stuff_on_creation
attr_accessor :accesor_for_the_instance
class << self
attr_accessor :accesor_for_the_class
end
end
module ClassMethods
#Methods in ClassMethods will get added class methods
def dummy?
puts "\nCLASS dummy? called\n"
true
end
end
#these methods are added as instance methods
def dummy?
puts "\nINSTANCE dummy? called\n"
true
end
def stuff_on_creation
puts "\ncreation called\n"
end
end
end
Just create a file under model/concerns/dummy.rb and include it
User.send :include, Concerns::Dummy
or
class User
include Concerns::Dummy end
Note that I repeat the namespace Concerns, but in other examples found in internet people dont. Rails 4 has included models/concerns and controllers/concerns in the autoload paths and in Rails 4 it would work without the namespace. In Rails 3, you need to add the namespace OR using some trick.
I find easier to understand if I include the 'Concern', tough.
Profit!
Saturday, February 8, 2014
Descubriendo los podcasts
Un podcast es basicamente un blog, pero de audio. Y lo realmente
importante es que hay miles de podcasts disponibles, con alta calidad y tratando temáticas muy variadas. Sólo tienes que bajarte los audios (mp3) y escucharlos cuando quieras.
--
Todo se inicia con un tweet de @david_bonilla
"Yo salgo a correr para tener una excusa para escuchar el@amuletodeyendor". Así descubro que "El amuleto de yendor" es un podcast sobre tecnología, dode 2 chavales hablan sobre cosas que me interesan.
El prime paso, fue descargarme el último programa desde ivoox : me lo escuche de una sentada pues no tiene desperdicio. ivoox está bien para descargarse programas antiguos, pero se queda corto a la hora de descubrir podcasts similares o buscar ciertas temáticas. Además yo quería que me avisaran cuando se grababan episodios nuevos (esto se llama subscripciones, y se maneja mediante rss).
--
Tras un mes de aprendizaje, así es como gestiono mis podcasts:
gPodder.net es el servicio que me permite guardar mis subscripciones . Es gratuito. Necesitas crear una cuenta y comenzar a añadir los podcasts que te interesen.
Lo siguiente es utilizar un programa que te descarge nuevos audios (episodios) según van apareciendo. Así te ahorras mucha gestion manual.
- para ordenadores tenemos clientes oficiales de gPodder en todas las plataformas http://gpodder.org/downloads así tengo mis WIndows y Linux sincronizados.
- para android yo utilizo ListeUp (no se cual es la diferencia entre la free o la pro de 1€, pero como es un programa que utilizo mucho, acabé pagando) . Con esto cubro mi MotoG y Nexus7 . Esta aplicacion tiene 2 cosas muy buenas: recuerda el audio que estábas escuchando la última vez que apagaste y que puedes manejar los controles desde la pantalla de bloqueo. También sincroniza con gPodder.
- para el iPad utilizo la aplicacion de podcasts oficial . No me gusta mucho porque no tiene integracion con gpodder, y tengo que añadir mis subscripciones a mano. De todas formas los podcasts los suelo escuchar en el movil o en ordenador, asi que no he investigado más.
- Creo que VLC tambien puede manejar subscripciones de podcasts, pero no está integrado con gpodder.
- si no te interesa la integracion con gpodder, en android tienes PodcastAddict, que es una pasada
- si viajas mucho en coche, puedes pasar los episodios a tu reproductor de mp3 y escucharlo en el coche.
Si utilizas gpodder en varios dispositivos, recuerda gestionar que todos están subscritos a las mismas fuentes y sincronizados, desde https://gpodder.net/devices/ Asi te evitas mucho trabajo.
--
Qué escucho?
Basicamente tecnología: iCharlas, PassionGeek, Infoxicados y el amuleto de Yendor.
Aqui estan mis subscripciones: en rss , opml o directamente la página de gpodder
Suelo descubrir nuevos podcasts, navegando por las subscripciones de otros usuarios que estan suscritos a mis mismos podcasts: algunas veces te llevas una sorpresa!
Dejo un enlace con uno de los episodios que mas me gustaron: Javifrechi (de infoxicados) en iCharlas, parte1 y parte2
--
Todo se inicia con un tweet de @david_bonilla
"Yo salgo a correr para tener una excusa para escuchar el
El prime paso, fue descargarme el último programa desde ivoox : me lo escuche de una sentada pues no tiene desperdicio. ivoox está bien para descargarse programas antiguos, pero se queda corto a la hora de descubrir podcasts similares o buscar ciertas temáticas. Además yo quería que me avisaran cuando se grababan episodios nuevos (esto se llama subscripciones, y se maneja mediante rss).
--
Tras un mes de aprendizaje, así es como gestiono mis podcasts:
gPodder.net es el servicio que me permite guardar mis subscripciones . Es gratuito. Necesitas crear una cuenta y comenzar a añadir los podcasts que te interesen.
Lo siguiente es utilizar un programa que te descarge nuevos audios (episodios) según van apareciendo. Así te ahorras mucha gestion manual.
- para ordenadores tenemos clientes oficiales de gPodder en todas las plataformas http://gpodder.org/downloads así tengo mis WIndows y Linux sincronizados.
- para android yo utilizo ListeUp (no se cual es la diferencia entre la free o la pro de 1€, pero como es un programa que utilizo mucho, acabé pagando) . Con esto cubro mi MotoG y Nexus7 . Esta aplicacion tiene 2 cosas muy buenas: recuerda el audio que estábas escuchando la última vez que apagaste y que puedes manejar los controles desde la pantalla de bloqueo. También sincroniza con gPodder.
- para el iPad utilizo la aplicacion de podcasts oficial . No me gusta mucho porque no tiene integracion con gpodder, y tengo que añadir mis subscripciones a mano. De todas formas los podcasts los suelo escuchar en el movil o en ordenador, asi que no he investigado más.
- Creo que VLC tambien puede manejar subscripciones de podcasts, pero no está integrado con gpodder.
- si no te interesa la integracion con gpodder, en android tienes PodcastAddict, que es una pasada
- si viajas mucho en coche, puedes pasar los episodios a tu reproductor de mp3 y escucharlo en el coche.
Si utilizas gpodder en varios dispositivos, recuerda gestionar que todos están subscritos a las mismas fuentes y sincronizados, desde https://gpodder.net/devices/ Asi te evitas mucho trabajo.
--
Qué escucho?
Basicamente tecnología: iCharlas, PassionGeek, Infoxicados y el amuleto de Yendor.
Aqui estan mis subscripciones: en rss , opml o directamente la página de gpodder
Suelo descubrir nuevos podcasts, navegando por las subscripciones de otros usuarios que estan suscritos a mis mismos podcasts: algunas veces te llevas una sorpresa!
Dejo un enlace con uno de los episodios que mas me gustaron: Javifrechi (de infoxicados) en iCharlas, parte1 y parte2
Wednesday, November 13, 2013
2 lines that must be on your <head>
In few words:
ensure that these lines are included in your <head>
ensure that these lines are included in your <head>
<meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
Thursday, September 5, 2013
Take a photo from Javascript
Finally it is 2013 and you can take photos from your browser without requiring flash.
The API that makes it possible is getUserMedia ( http://caniuse.com/#search=getUserMedia ) and it is available in all modern browsers (requiring some vendor-prefixes, though).
For demo: use this fiddle
The code I paste here, takes a photo, dump it to a canvas and tries to upload it to a (non-existent) server.
- The key here is that both the captured image AND the uploaded image does not need to be the same size (you usually don't want to upload very big files). That is controlled via the OUTPUT_RATIO constant: the final size is given by the output canvas.
- Neither the video or the output are required to be shown, however is good to give visual feedback to users.
- NOTE: Chrome does not allow local files to get access to getUserMedia. You can use fiddle to make the test yourself.
- At the present, the api still needs to be prefixed depending on the browsers:
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
The API that makes it possible is getUserMedia ( http://caniuse.com/#search=getUserMedia ) and it is available in all modern browsers (requiring some vendor-prefixes, though).
For demo: use this fiddle
The code I paste here, takes a photo, dump it to a canvas and tries to upload it to a (non-existent) server.
- The key here is that both the captured image AND the uploaded image does not need to be the same size (you usually don't want to upload very big files). That is controlled via the OUTPUT_RATIO constant: the final size is given by the output canvas.
- Neither the video or the output are required to be shown, however is good to give visual feedback to users.
- NOTE: Chrome does not allow local files to get access to getUserMedia. You can use fiddle to make the test yourself.
- At the present, the api still needs to be prefixed depending on the browsers:
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
<!DOCTYPE html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <title>getUserApi</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width"> <style type="text/css"> video { background: rgba(255,255,255,0.5); border: 1px solid #ccc; } </style> </head> <body> <div id='text'> <p>You must grant access to the Camera first.</p> <p>Prompt would be shown above this lines, next to the address bar.</p> <button type='button' id='button'>Take photo</button> </div> <video id='video' width="640" height="480"></video> <canvas id='canvas' style="display:none;"></canvas> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.js"></script> <script> (function(window) { var nav = window.navigator, doc = window.document, //some browsers behave differently is_webkit = nav.webkitGetUserMedia, is_mozilla = nav.mozGetUserMedia, showSnapshot = true, showVideo = true, OUTPUT_RATIO = 0.5, //the output is X times the captured image (ex: we upload small photos) source, video, canvas, button, ctx, localMediaStream; var initCamera = function() { video = document.getElementById('video'), canvas = document.getElementById('canvas'), button = document.getElementById('button'), ctx = canvas.getContext('2d'); //make canvas and video the same dimensions canvas.width = video.width * OUTPUT_RATIO | 0; canvas.height = video.height * OUTPUT_RATIO | 0; //turn canvas to visible canvas.style.display = showSnapshot ? '' : 'none'; video.style.display = showVideo ? '' : 'none'; (button || video).addEventListener('click', takeSnapshot, false); //addEventListener: IE9+ Opera7+ Safari, FFox, Chrome // if (is_webkit){ // nav.getUserMedia('video', onSuccess, onError); // }else{ nav.getUserMedia({ video: true }, onSuccess, onError); // } }, onError = function(e) { alert('Camera permission rejected!', e); }, onSuccess = function(stream) { if (is_mozilla) { source = window.URL.createObjectURL(stream); } else if (is_webkit) { source = window.webkitURL.createObjectURL(stream); } else { source = stream; } video.src = source; video.play(); localMediaStream = stream; }, stopCamera = function(){ localMediaStream.stop(); video.style.display = canvas.style.display = 'none'; localMediaStream = canvas = ctx = null; button }, takeSnapshot = function() { if (localMediaStream) { ctx.drawImage(video, 0, 0, (video.width * OUTPUT_RATIO) | 0, (video.height * OUTPUT_RATIO) | 0); uploadSnapshot(); } }, uploadSnapshot = function(){ var dataUrl; try { dataUrl = canvas.toDataURL('image/jpeg', 1).split(',')[1]; } catch(e) { dataUrl = canvas.toDataURL().split(',')[1]; } $.ajax({ url: "localhost:3000/uploadTest", type: "POST", data: {imagedata : dataUrl}, //in the server file.write(Base64.decode64(imagedata)) , https://gist.github.com/pierrevalade/397615 contentType: "application/json; charset=utf-8", dataType: "json", success: function () { alert('Image Uploaded!!'); }, error: function () { alert("There was some error while uploading Image"); } }); }; //some browsers use prefixes nav.getUserMedia = nav.getUserMedia || nav.webkitGetUserMedia || nav.mozGetUserMedia || nav.msGetUserMedia; if (nav.getUserMedia) { initCamera(); } else { alert("Your browser does not support getUserMedia()") } }(this)); </script> </body> </html>
Bonus
For large images you can save some bytes by sending the image as a blob instead of a base64 encoding.
First, encode the dataUrl as a blob
function dataURItoBlob(dataURI, dataTYPE) {
var binary = atob(dataURI), array = [];
for(var i = 0; i < binary.length; i++) array.push(binary.charCodeAt(i));
return new Blob([new Uint8Array(array)], {type: dataTYPE});
}
var binary = atob(dataURI), array = [];
for(var i = 0; i < binary.length; i++) array.push(binary.charCodeAt(i));
return new Blob([new Uint8Array(array)], {type: dataTYPE});
}
Then, you have 2 options:
- using the FormData api
function uploadWithFormData(dataUrl){
// Get our file
var file = dataURItoBlob(dataUrl, 'image/jpeg'),
fd = new FormData();
// Append our Canvas image file to the form data
fd.append("imageNameHere", file);
// And send it
$.ajax({
url: "/server",
type: "POST",
data: fd,
processData: false,
contentType: false,
});
}
// Get our file
var file = dataURItoBlob(dataUrl, 'image/jpeg'),
fd = new FormData();
// Append our Canvas image file to the form data
fd.append("imageNameHere", file);
// And send it
$.ajax({
url: "/server",
type: "POST",
data: fd,
processData: false,
contentType: false,
});
}
- or using the XHR
function uploadWithXHR(dataUrl) {
var file = dataURItoBlob(dataUrl, 'image/jpeg'),
xhr = new XMLHttpRequest();
xhr.open('POST', '/server', true);
//add the headers you need
// xhr.setRequestHeader("Cache-Control", "no-cache");
// xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// xhr.setRequestHeader("X-File-Name", file.name || file.fileName || 'image.jpg');
// xhr.setRequestHeader("X-File-Size", file.size || file.fileSize);
// xhr.setRequestHeader("X-File-Type", file.type);
// xhr.setRequestHeader("Content-Type", options.type);
// xhr.setRequestHeader("Accept","application/json, text/javascript, */*; q=0.01");
xhr.send(file);
}
var file = dataURItoBlob(dataUrl, 'image/jpeg'),
xhr = new XMLHttpRequest();
xhr.open('POST', '/server', true);
//add the headers you need
// xhr.setRequestHeader("Cache-Control", "no-cache");
// xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// xhr.setRequestHeader("X-File-Name", file.name || file.fileName || 'image.jpg');
// xhr.setRequestHeader("X-File-Size", file.size || file.fileSize);
// xhr.setRequestHeader("X-File-Type", file.type);
// xhr.setRequestHeader("Content-Type", options.type);
// xhr.setRequestHeader("Accept","application/json, text/javascript, */*; q=0.01");
xhr.send(file);
}
Subscribe to:
Posts (Atom)