2127 lines
211 KiB
HTML
2127 lines
211 KiB
HTML
|
<!DOCTYPE html><html lang="fr">
|
|||
|
<head><meta charset="utf-8">
|
|||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"><title>Installer LineageOS sur son appareil Android - YannStatic</title>
|
|||
|
|
|||
|
<meta name="description" content="Lorsque j’ai installé LineageOS sur mon smartphone, j’ai suivi des tutoriels sans réellement comprendre ce que je faisais. Je voulais donc écrire cette dépêc...">
|
|||
|
<link rel="canonical" href="https://static.rnmkcy.eu/2019/07/06/Installer-LineageOS-sur-appareil-Android.html"><link rel="alternate" type="application/rss+xml" title="YannStatic" href="/feed.xml">
|
|||
|
|
|||
|
<!-- - include head/favicon.html - -->
|
|||
|
<link rel="shortcut icon" type="image/png" href="/assets/favicon/favicon.png"><link rel="stylesheet" href="/assets/css/main.css"><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" ><!-- start custom head snippets --><link rel="stylesheet" href="/assets/css/expand.css">
|
|||
|
<!-- end custom head snippets --><script>(function() {
|
|||
|
window.isArray = function(val) {
|
|||
|
return Object.prototype.toString.call(val) === '[object Array]';
|
|||
|
};
|
|||
|
window.isString = function(val) {
|
|||
|
return typeof val === 'string';
|
|||
|
};
|
|||
|
|
|||
|
window.hasEvent = function(event) {
|
|||
|
return 'on'.concat(event) in window.document;
|
|||
|
};
|
|||
|
|
|||
|
window.isOverallScroller = function(node) {
|
|||
|
return node === document.documentElement || node === document.body || node === window;
|
|||
|
};
|
|||
|
|
|||
|
window.isFormElement = function(node) {
|
|||
|
var tagName = node.tagName;
|
|||
|
return tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
|
|||
|
};
|
|||
|
|
|||
|
window.pageLoad = (function () {
|
|||
|
var loaded = false, cbs = [];
|
|||
|
window.addEventListener('load', function () {
|
|||
|
var i;
|
|||
|
loaded = true;
|
|||
|
if (cbs.length > 0) {
|
|||
|
for (i = 0; i < cbs.length; i++) {
|
|||
|
cbs[i]();
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
return {
|
|||
|
then: function(cb) {
|
|||
|
cb && (loaded ? cb() : (cbs.push(cb)));
|
|||
|
}
|
|||
|
};
|
|||
|
})();
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
window.throttle = function(func, wait) {
|
|||
|
var args, result, thisArg, timeoutId, lastCalled = 0;
|
|||
|
|
|||
|
function trailingCall() {
|
|||
|
lastCalled = new Date;
|
|||
|
timeoutId = null;
|
|||
|
result = func.apply(thisArg, args);
|
|||
|
}
|
|||
|
return function() {
|
|||
|
var now = new Date,
|
|||
|
remaining = wait - (now - lastCalled);
|
|||
|
|
|||
|
args = arguments;
|
|||
|
thisArg = this;
|
|||
|
|
|||
|
if (remaining <= 0) {
|
|||
|
clearTimeout(timeoutId);
|
|||
|
timeoutId = null;
|
|||
|
lastCalled = now;
|
|||
|
result = func.apply(thisArg, args);
|
|||
|
} else if (!timeoutId) {
|
|||
|
timeoutId = setTimeout(trailingCall, remaining);
|
|||
|
}
|
|||
|
return result;
|
|||
|
};
|
|||
|
};
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var Set = (function() {
|
|||
|
var add = function(item) {
|
|||
|
var i, data = this._data;
|
|||
|
for (i = 0; i < data.length; i++) {
|
|||
|
if (data[i] === item) {
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
this.size ++;
|
|||
|
data.push(item);
|
|||
|
return data;
|
|||
|
};
|
|||
|
|
|||
|
var Set = function(data) {
|
|||
|
this.size = 0;
|
|||
|
this._data = [];
|
|||
|
var i;
|
|||
|
if (data.length > 0) {
|
|||
|
for (i = 0; i < data.length; i++) {
|
|||
|
add.call(this, data[i]);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
Set.prototype.add = add;
|
|||
|
Set.prototype.get = function(index) { return this._data[index]; };
|
|||
|
Set.prototype.has = function(item) {
|
|||
|
var i, data = this._data;
|
|||
|
for (i = 0; i < data.length; i++) {
|
|||
|
if (this.get(i) === item) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
};
|
|||
|
Set.prototype.is = function(map) {
|
|||
|
if (map._data.length !== this._data.length) { return false; }
|
|||
|
var i, j, flag, tData = this._data, mData = map._data;
|
|||
|
for (i = 0; i < tData.length; i++) {
|
|||
|
for (flag = false, j = 0; j < mData.length; j++) {
|
|||
|
if (tData[i] === mData[j]) {
|
|||
|
flag = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (!flag) { return false; }
|
|||
|
}
|
|||
|
return true;
|
|||
|
};
|
|||
|
Set.prototype.values = function() {
|
|||
|
return this._data;
|
|||
|
};
|
|||
|
return Set;
|
|||
|
})();
|
|||
|
|
|||
|
window.Lazyload = (function(doc) {
|
|||
|
var queue = {js: [], css: []}, sources = {js: {}, css: {}}, context = this;
|
|||
|
var createNode = function(name, attrs) {
|
|||
|
var node = doc.createElement(name), attr;
|
|||
|
for (attr in attrs) {
|
|||
|
if (attrs.hasOwnProperty(attr)) {
|
|||
|
node.setAttribute(attr, attrs[attr]);
|
|||
|
}
|
|||
|
}
|
|||
|
return node;
|
|||
|
};
|
|||
|
var end = function(type, url) {
|
|||
|
var s, q, qi, cbs, i, j, cur, val, flag;
|
|||
|
if (type === 'js' || type ==='css') {
|
|||
|
s = sources[type], q = queue[type];
|
|||
|
s[url] = true;
|
|||
|
for (i = 0; i < q.length; i++) {
|
|||
|
cur = q[i];
|
|||
|
if (cur.urls.has(url)) {
|
|||
|
qi = cur, val = qi.urls.values();
|
|||
|
qi && (cbs = qi.callbacks);
|
|||
|
for (flag = true, j = 0; j < val.length; j++) {
|
|||
|
cur = val[j];
|
|||
|
if (!s[cur]) {
|
|||
|
flag = false;
|
|||
|
}
|
|||
|
}
|
|||
|
if (flag && cbs && cbs.length > 0) {
|
|||
|
for (j = 0; j < cbs.length; j++) {
|
|||
|
cbs[j].call(context);
|
|||
|
}
|
|||
|
qi.load = true;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
var load = function(type, urls, callback) {
|
|||
|
var s, q, qi, node, i, cur,
|
|||
|
_urls = typeof urls === 'string' ? new Set([urls]) : new Set(urls), val, url;
|
|||
|
if (type === 'js' || type ==='css') {
|
|||
|
s = sources[type], q = queue[type];
|
|||
|
for (i = 0; i < q.length; i++) {
|
|||
|
cur = q[i];
|
|||
|
if (_urls.is(cur.urls)) {
|
|||
|
qi = cur;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
val = _urls.values();
|
|||
|
if (qi) {
|
|||
|
callback && (qi.load || qi.callbacks.push(callback));
|
|||
|
callback && (qi.load && callback());
|
|||
|
} else {
|
|||
|
q.push({
|
|||
|
urls: _urls,
|
|||
|
callbacks: callback ? [callback] : [],
|
|||
|
load: false
|
|||
|
});
|
|||
|
for (i = 0; i < val.length; i++) {
|
|||
|
node = null, url = val[i];
|
|||
|
if (s[url] === undefined) {
|
|||
|
(type === 'js' ) && (node = createNode('script', { src: url }));
|
|||
|
(type === 'css') && (node = createNode('link', { rel: 'stylesheet', href: url }));
|
|||
|
if (node) {
|
|||
|
node.onload = (function(type, url) {
|
|||
|
return function() {
|
|||
|
end(type, url);
|
|||
|
};
|
|||
|
})(type, url);
|
|||
|
(doc.head || doc.body).appendChild(node);
|
|||
|
s[url] = false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
return {
|
|||
|
js: function(url, callback) {
|
|||
|
load('js', url, callback);
|
|||
|
},
|
|||
|
css: function(url, callback) {
|
|||
|
load('css', url, callback);
|
|||
|
}
|
|||
|
};
|
|||
|
})(this.document);
|
|||
|
})();
|
|||
|
</script><script>
|
|||
|
(function() {
|
|||
|
var TEXT_VARIABLES = {
|
|||
|
version: '2.2.6',
|
|||
|
sources: {
|
|||
|
font_awesome: 'https://use.fontawesome.com/releases/v5.0.13/css/all.css',
|
|||
|
jquery: '/assets/js/jquery.min.js',
|
|||
|
leancloud_js_sdk: '//cdn.jsdelivr.net/npm/leancloud-storage@3.13.2/dist/av-min.js',
|
|||
|
chart: 'https://cdn.bootcss.com/Chart.js/2.7.2/Chart.bundle.min.js',
|
|||
|
gitalk: {
|
|||
|
js: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.js',
|
|||
|
css: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.css'
|
|||
|
},
|
|||
|
valine: 'https://unpkg.com/valine/dist/Valine.min.js'
|
|||
|
},
|
|||
|
site: {
|
|||
|
toc: {
|
|||
|
selectors: 'h1,h2,h3'
|
|||
|
}
|
|||
|
},
|
|||
|
paths: {
|
|||
|
search_js: '/assets/search.js'
|
|||
|
}
|
|||
|
};
|
|||
|
window.TEXT_VARIABLES = TEXT_VARIABLES;
|
|||
|
})();
|
|||
|
</script>
|
|||
|
</head>
|
|||
|
<body>
|
|||
|
<div class="root" data-is-touch="false">
|
|||
|
<div class="layout--page js-page-root"><!----><div class="page__main js-page-main page__viewport hide-footer has-aside has-aside cell cell--auto">
|
|||
|
|
|||
|
<div class="page__main-inner"><div class="page__header d-print-none"><header class="header"><div class="main">
|
|||
|
<div class="header__title">
|
|||
|
<div class="header__brand"><svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="478.9473684210526" viewBox="0, 0, 400,478.9473684210526"><g id="svgg"><path id="path0" d="M308.400 56.805 C 306.970 56.966,303.280 57.385,300.200 57.738 C 290.906 58.803,278.299 59.676,269.200 59.887 L 260.600 60.085 259.400 61.171 C 258.010 62.428,256.198 63.600,255.645 63.600 C 255.070 63.600,252.887 65.897,252.598 66.806 C 252.460 67.243,252.206 67.600,252.034 67.600 C 251.397 67.600,247.206 71.509,247.202 72.107 C 247.201 72.275,246.390 73.190,245.400 74.138 C 243.961 75.517,243.598 76.137,243.592 77.231 C 243.579 79.293,241.785 83.966,240.470 85.364 C 239.176 86.740,238.522 88.365,237.991 91.521 C 237.631 93.665,236.114 97.200,235.554 97.200 C 234.938 97.200,232.737 102.354,232.450 104.472 C 232.158 106.625,230.879 109.226,229.535 110.400 C 228.933 110.926,228.171 113.162,226.434 119.500 C 226.178 120.435,225.795 121.200,225.584 121.200 C 225.373 121.200,225.200 121.476,225.200 121.813 C 225.200 122.149,224.885 122.541,224.500 122.683 C 223.606 123.013,223.214 123.593,223.204 124.600 C 223.183 126.555,220.763 132.911,219.410 134.562 C 218.443 135.742,217.876 136.956,217.599 138.440 C 217.041 141.424,215.177 146.434,214.532 146.681 C 214.240 146.794,214.000 147.055,214.000 147.261 C 214.000 147.467,213.550 148.086,213.000 148.636 C 212.450 149.186,212.000 149.893,212.000 150.208 C 212.000 151.386,208.441 154.450,207.597 153.998 C 206.319 153.315,204.913 150.379,204.633 147.811 C 204.365 145.357,202.848 142.147,201.759 141.729 C 200.967 141.425,199.200 137.451,199.200 135.974 C 199.200 134.629,198.435 133.224,196.660 131.311 C 195.363 129.913,194.572 128.123,193.870 125.000 C 193.623 123.900,193.236 122.793,193.010 122.540 C 190.863 120.133,190.147 118.880,188.978 115.481 C 188.100 112.928,187.151 111.003,186.254 109.955 C 185.358 108.908,184.518 107.204,183.847 105.073 C 183.280 103.273,182.497 101.329,182.108 100.753 C 181.719 100.177,180.904 98.997,180.298 98.131 C 179.693 97.265,178.939 95.576,178.624 94.378 C 178.041 92.159,177.125 90.326,175.023 87.168 C 174.375 86.196,173.619 84.539,173.342 83.486 C 172.800 81.429,171.529 79.567,170.131 78.785 C 169.654 78.517,168.697 77.511,168.006 76.549 C 167.316 75.587,166.594 74.800,166.402 74.800 C 166.210 74.800,164.869 73.633,163.421 72.206 C 160.103 68.936,161.107 69.109,146.550 69.301 C 133.437 69.474,128.581 70.162,126.618 72.124 C 126.248 72.495,125.462 72.904,124.872 73.033 C 124.282 73.163,123.088 73.536,122.219 73.863 C 121.349 74.191,119.028 74.638,117.061 74.858 C 113.514 75.254,109.970 76.350,108.782 77.419 C 107.652 78.436,100.146 80.400,97.388 80.400 C 95.775 80.400,93.167 81.360,91.200 82.679 C 90.430 83.195,89.113 83.804,88.274 84.031 C 85.875 84.681,78.799 90.910,74.400 96.243 L 73.400 97.456 73.455 106.028 C 73.526 117.055,74.527 121.238,77.820 124.263 C 78.919 125.273,80.400 127.902,80.400 128.842 C 80.400 129.202,81.075 130.256,81.900 131.186 C 83.563 133.059,85.497 136.346,86.039 138.216 C 86.233 138.886,87.203 140.207,88.196 141.153 C 89.188 142.098,90.000 143.104,90.000 143.388 C 90.000 144.337,92.129 148.594,92.869 149.123 C 93.271 149.410,93.600 149.831,93.600 150.059 C 93.600 150.286,93.932 150.771,94.337 151.136 C 94.743 151.501,95.598 153.004,96.237 154.475 C 96.877 155.947,97.760 157.351,98.200 157.596 C 98.640 157.841,99.900 159.943,101.000 162.267 C 102.207 164.817,103.327 166.644,103.825 166.876 C 104.278 167.087,105.065 168.101,105.573 169.130 C 107.658 173.348,108.097 174.093,110.006 176.647 C 111.103 178.114,112.000 179.725,112.000 180.227 C 112.000 181.048,113.425 183.163,114.678 184.200 C 115.295 184.711,117.396 188.733,117.720 190.022 C 117.855 190.562,118.603 191.633,119.381 192.402 C 120.160 193.171,121.496 195.258,122.351 197.039 C 123.206 198.820,124.167 200.378,124.487 200.501 C 124.807 200.624,125.953 202.496,127.034 204.662 C 128.114 206.828,129.676 209.299,130.505 210.153 C 131.333 211.007,132.124 212.177,132.262 212.753 C 132.618 214.239,134.291 217.048,136.288 219.5
|
|||
|
" href="/">YannStatic</a></div><!--<button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button>--><!-- <li><button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button></li> -->
|
|||
|
<!-- Champ de recherche -->
|
|||
|
<div id="searchbox" class="search search--dark" style="visibility: visible">
|
|||
|
<div class="main">
|
|||
|
<div class="search__header"></div>
|
|||
|
<div class="search-bar">
|
|||
|
<div class="search-box js-search-box">
|
|||
|
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
|
|||
|
<input id="search-input" type="text" />
|
|||
|
<!-- <div class="search-box__icon-clear js-icon-clear">
|
|||
|
<a><i class="fas fa-times"></i></a>
|
|||
|
</div> -->
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<!-- Script pointing to search-script.js -->
|
|||
|
<script>/*!
|
|||
|
* Simple-Jekyll-Search
|
|||
|
* Copyright 2015-2020, Christian Fei
|
|||
|
* Licensed under the MIT License.
|
|||
|
*/
|
|||
|
|
|||
|
(function(){
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Templater_7 = {
|
|||
|
compile: compile,
|
|||
|
setOptions: setOptions
|
|||
|
}
|
|||
|
|
|||
|
const options = {}
|
|||
|
options.pattern = /\{(.*?)\}/g
|
|||
|
options.template = ''
|
|||
|
options.middleware = function () {}
|
|||
|
|
|||
|
function setOptions (_options) {
|
|||
|
options.pattern = _options.pattern || options.pattern
|
|||
|
options.template = _options.template || options.template
|
|||
|
if (typeof _options.middleware === 'function') {
|
|||
|
options.middleware = _options.middleware
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function compile (data) {
|
|||
|
return options.template.replace(options.pattern, function (match, prop) {
|
|||
|
const value = options.middleware(prop, data[prop], options.template)
|
|||
|
if (typeof value !== 'undefined') {
|
|||
|
return value
|
|||
|
}
|
|||
|
return data[prop] || match
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
'use strict';
|
|||
|
|
|||
|
function fuzzysearch (needle, haystack) {
|
|||
|
var tlen = haystack.length;
|
|||
|
var qlen = needle.length;
|
|||
|
if (qlen > tlen) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (qlen === tlen) {
|
|||
|
return needle === haystack;
|
|||
|
}
|
|||
|
outer: for (var i = 0, j = 0; i < qlen; i++) {
|
|||
|
var nch = needle.charCodeAt(i);
|
|||
|
while (j < tlen) {
|
|||
|
if (haystack.charCodeAt(j++) === nch) {
|
|||
|
continue outer;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
var _$fuzzysearch_1 = fuzzysearch;
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
|
|||
|
|
|||
|
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
|
|||
|
|
|||
|
function FuzzySearchStrategy () {
|
|||
|
this.matches = function (string, crit) {
|
|||
|
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
|
|||
|
|
|||
|
function LiteralSearchStrategy () {
|
|||
|
this.matches = function (str, crit) {
|
|||
|
if (!str) return false
|
|||
|
|
|||
|
str = str.trim().toLowerCase()
|
|||
|
crit = crit.trim().toLowerCase()
|
|||
|
|
|||
|
return crit.split(' ').filter(function (word) {
|
|||
|
return str.indexOf(word) >= 0
|
|||
|
}).length === crit.split(' ').length
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Repository_4 = {
|
|||
|
put: put,
|
|||
|
clear: clear,
|
|||
|
search: search,
|
|||
|
setOptions: __setOptions_4
|
|||
|
}
|
|||
|
|
|||
|
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
|
|||
|
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
|
|||
|
|
|||
|
function NoSort () {
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
const data = []
|
|||
|
let opt = {}
|
|||
|
|
|||
|
opt.fuzzy = false
|
|||
|
opt.limit = 10
|
|||
|
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = NoSort
|
|||
|
opt.exclude = []
|
|||
|
|
|||
|
function put (data) {
|
|||
|
if (isObject(data)) {
|
|||
|
return addObject(data)
|
|||
|
}
|
|||
|
if (isArray(data)) {
|
|||
|
return addArray(data)
|
|||
|
}
|
|||
|
return undefined
|
|||
|
}
|
|||
|
function clear () {
|
|||
|
data.length = 0
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function isObject (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
|
|||
|
}
|
|||
|
|
|||
|
function isArray (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
|
|||
|
}
|
|||
|
|
|||
|
function addObject (_data) {
|
|||
|
data.push(_data)
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function addArray (_data) {
|
|||
|
const added = []
|
|||
|
clear()
|
|||
|
for (let i = 0, len = _data.length; i < len; i++) {
|
|||
|
if (isObject(_data[i])) {
|
|||
|
added.push(addObject(_data[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
return added
|
|||
|
}
|
|||
|
|
|||
|
function search (crit) {
|
|||
|
if (!crit) {
|
|||
|
return []
|
|||
|
}
|
|||
|
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
|
|||
|
}
|
|||
|
|
|||
|
function __setOptions_4 (_opt) {
|
|||
|
opt = _opt || {}
|
|||
|
|
|||
|
opt.fuzzy = _opt.fuzzy || false
|
|||
|
opt.limit = _opt.limit || 10
|
|||
|
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = _opt.sort || NoSort
|
|||
|
opt.exclude = _opt.exclude || []
|
|||
|
}
|
|||
|
|
|||
|
function findMatches (data, crit, strategy, opt) {
|
|||
|
const matches = []
|
|||
|
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
|
|||
|
const match = findMatchesInObject(data[i], crit, strategy, opt)
|
|||
|
if (match) {
|
|||
|
matches.push(match)
|
|||
|
}
|
|||
|
}
|
|||
|
return matches
|
|||
|
}
|
|||
|
|
|||
|
function findMatchesInObject (obj, crit, strategy, opt) {
|
|||
|
for (const key in obj) {
|
|||
|
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
|
|||
|
return obj
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isExcluded (term, excludedTerms) {
|
|||
|
for (let i = 0, len = excludedTerms.length; i < len; i++) {
|
|||
|
const excludedTerm = excludedTerms[i]
|
|||
|
if (new RegExp(excludedTerm).test(term)) {
|
|||
|
return true
|
|||
|
}
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
|
|||
|
/* globals ActiveXObject:false */
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$JSONLoader_2 = {
|
|||
|
load: load
|
|||
|
}
|
|||
|
|
|||
|
function load (location, callback) {
|
|||
|
const xhr = getXHR()
|
|||
|
xhr.open('GET', location, true)
|
|||
|
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
|
|||
|
xhr.send()
|
|||
|
}
|
|||
|
|
|||
|
function createStateChangeListener (xhr, callback) {
|
|||
|
return function () {
|
|||
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|||
|
try {
|
|||
|
callback(null, JSON.parse(xhr.responseText))
|
|||
|
} catch (err) {
|
|||
|
callback(err, null)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function getXHR () {
|
|||
|
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$OptionsValidator_3 = function OptionsValidator (params) {
|
|||
|
if (!validateParams(params)) {
|
|||
|
throw new Error('-- OptionsValidator: required options missing')
|
|||
|
}
|
|||
|
|
|||
|
if (!(this instanceof OptionsValidator)) {
|
|||
|
return new OptionsValidator(params)
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = params.required
|
|||
|
|
|||
|
this.getRequiredOptions = function () {
|
|||
|
return requiredOptions
|
|||
|
}
|
|||
|
|
|||
|
this.validate = function (parameters) {
|
|||
|
const errors = []
|
|||
|
requiredOptions.forEach(function (requiredOptionName) {
|
|||
|
if (typeof parameters[requiredOptionName] === 'undefined') {
|
|||
|
errors.push(requiredOptionName)
|
|||
|
}
|
|||
|
})
|
|||
|
return errors
|
|||
|
}
|
|||
|
|
|||
|
function validateParams (params) {
|
|||
|
if (!params) {
|
|||
|
return false
|
|||
|
}
|
|||
|
return typeof params.required !== 'undefined' && params.required instanceof Array
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$utils_9 = {
|
|||
|
merge: merge,
|
|||
|
isJSON: isJSON
|
|||
|
}
|
|||
|
|
|||
|
function merge (defaultParams, mergeParams) {
|
|||
|
const mergedOptions = {}
|
|||
|
for (const option in defaultParams) {
|
|||
|
mergedOptions[option] = defaultParams[option]
|
|||
|
if (typeof mergeParams[option] !== 'undefined') {
|
|||
|
mergedOptions[option] = mergeParams[option]
|
|||
|
}
|
|||
|
}
|
|||
|
return mergedOptions
|
|||
|
}
|
|||
|
|
|||
|
function isJSON (json) {
|
|||
|
try {
|
|||
|
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
|
|||
|
return true
|
|||
|
}
|
|||
|
return false
|
|||
|
} catch (err) {
|
|||
|
return false
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var _$src_8 = {};
|
|||
|
(function (window) {
|
|||
|
'use strict'
|
|||
|
|
|||
|
let options = {
|
|||
|
searchInput: null,
|
|||
|
resultsContainer: null,
|
|||
|
json: [],
|
|||
|
success: Function.prototype,
|
|||
|
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
|
|||
|
templateMiddleware: Function.prototype,
|
|||
|
sortMiddleware: function () {
|
|||
|
return 0
|
|||
|
},
|
|||
|
noResultsText: 'No results found',
|
|||
|
limit: 10,
|
|||
|
fuzzy: false,
|
|||
|
debounceTime: null,
|
|||
|
exclude: []
|
|||
|
}
|
|||
|
|
|||
|
let debounceTimerHandle
|
|||
|
const debounce = function (func, delayMillis) {
|
|||
|
if (delayMillis) {
|
|||
|
clearTimeout(debounceTimerHandle)
|
|||
|
debounceTimerHandle = setTimeout(func, delayMillis)
|
|||
|
} else {
|
|||
|
func.call()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
|
|||
|
|
|||
|
/* removed: const _$Templater_7 = require('./Templater') */;
|
|||
|
/* removed: const _$Repository_4 = require('./Repository') */;
|
|||
|
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
|
|||
|
const optionsValidator = _$OptionsValidator_3({
|
|||
|
required: requiredOptions
|
|||
|
})
|
|||
|
/* removed: const _$utils_9 = require('./utils') */;
|
|||
|
|
|||
|
window.SimpleJekyllSearch = function (_options) {
|
|||
|
const errors = optionsValidator.validate(_options)
|
|||
|
if (errors.length > 0) {
|
|||
|
throwError('You must specify the following required options: ' + requiredOptions)
|
|||
|
}
|
|||
|
|
|||
|
options = _$utils_9.merge(options, _options)
|
|||
|
|
|||
|
_$Templater_7.setOptions({
|
|||
|
template: options.searchResultTemplate,
|
|||
|
middleware: options.templateMiddleware
|
|||
|
})
|
|||
|
|
|||
|
_$Repository_4.setOptions({
|
|||
|
fuzzy: options.fuzzy,
|
|||
|
limit: options.limit,
|
|||
|
sort: options.sortMiddleware,
|
|||
|
exclude: options.exclude
|
|||
|
})
|
|||
|
|
|||
|
if (_$utils_9.isJSON(options.json)) {
|
|||
|
initWithJSON(options.json)
|
|||
|
} else {
|
|||
|
initWithURL(options.json)
|
|||
|
}
|
|||
|
|
|||
|
const rv = {
|
|||
|
search: search
|
|||
|
}
|
|||
|
|
|||
|
typeof options.success === 'function' && options.success.call(rv)
|
|||
|
return rv
|
|||
|
}
|
|||
|
|
|||
|
function initWithJSON (json) {
|
|||
|
_$Repository_4.put(json)
|
|||
|
registerInput()
|
|||
|
}
|
|||
|
|
|||
|
function initWithURL (url) {
|
|||
|
_$JSONLoader_2.load(url, function (err, json) {
|
|||
|
if (err) {
|
|||
|
throwError('failed to get JSON (' + url + ')')
|
|||
|
}
|
|||
|
initWithJSON(json)
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function emptyResultsContainer () {
|
|||
|
options.resultsContainer.innerHTML = ''
|
|||
|
}
|
|||
|
|
|||
|
function appendToResultsContainer (text) {
|
|||
|
options.resultsContainer.innerHTML += text
|
|||
|
}
|
|||
|
|
|||
|
function registerInput () {
|
|||
|
options.searchInput.addEventListener('input', function (e) {
|
|||
|
if (isWhitelistedKey(e.which)) {
|
|||
|
emptyResultsContainer()
|
|||
|
debounce(function () { search(e.target.value) }, options.debounceTime)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function search (query) {
|
|||
|
if (isValidQuery(query)) {
|
|||
|
emptyResultsContainer()
|
|||
|
render(_$Repository_4.search(query), query)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function render (results, query) {
|
|||
|
const len = results.length
|
|||
|
if (len === 0) {
|
|||
|
return appendToResultsContainer(options.noResultsText)
|
|||
|
}
|
|||
|
for (let i = 0; i < len; i++) {
|
|||
|
results[i].query = query
|
|||
|
appendToResultsContainer(_$Templater_7.compile(results[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isValidQuery (query) {
|
|||
|
return query && query.length > 0
|
|||
|
}
|
|||
|
|
|||
|
function isWhitelistedKey (key) {
|
|||
|
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
|
|||
|
}
|
|||
|
|
|||
|
function throwError (message) {
|
|||
|
throw new Error('SimpleJekyllSearch --- ' + message)
|
|||
|
}
|
|||
|
})(window)
|
|||
|
|
|||
|
}());
|
|||
|
</script>
|
|||
|
|
|||
|
<!-- Configuration -->
|
|||
|
<script>
|
|||
|
SimpleJekyllSearch({
|
|||
|
searchInput: document.getElementById('search-input'),
|
|||
|
resultsContainer: document.getElementById('results-container'),
|
|||
|
json: '/search.json',
|
|||
|
//searchResultTemplate: '<li><a href="https://static.rnmkcy.eu{url}">{date} {title}</a></li>'
|
|||
|
searchResultTemplate: '<li><a href="{url}">{date} {title}</a></li>'
|
|||
|
})
|
|||
|
</script>
|
|||
|
<!-- Fin déclaration champ de recherche --></div><nav class="navigation">
|
|||
|
<ul><li class="navigation__item"><a href="/archive.html">Etiquettes</a></li><li class="navigation__item"><a href="/htmldoc.html">Documents</a></li><li class="navigation__item"><a href="/liens_ttrss.html">Liens</a></li><li class="navigation__item"><a href="/aide-jekyll-text-theme.html">Aide</a></li></ul>
|
|||
|
</nav></div>
|
|||
|
</header>
|
|||
|
|
|||
|
</div><div class="page__content"><div class ="main"><div class="grid grid--reverse">
|
|||
|
<div class="col-main cell cell--auto"><!-- start custom main top snippet --><div id="results-container" class="search-result js-search-result"></div><!-- end custom main top snippet -->
|
|||
|
<article itemscope itemtype="http://schema.org/Article"><div class="article__header"><header><h1 style="color:Tomato;">Installer LineageOS sur son appareil Android</h1></header></div><meta itemprop="headline" content="Installer LineageOS sur son appareil Android"><div class="article__info clearfix"><ul class="left-col menu"><li>
|
|||
|
<a class="button button--secondary button--pill button--sm"
|
|||
|
href="/archive.html?tag=android">android</a>
|
|||
|
</li></ul><ul class="right-col menu"><li>
|
|||
|
<i class="far fa-calendar-alt"></i> <span title="Création" style="color:#FF00FF"> 6 juil. 2019</span></li></ul></div><meta itemprop="datePublished" content="2019-07-06T00:00:00+02:00">
|
|||
|
<meta itemprop="keywords" content="android"><div class="js-article-content">
|
|||
|
<div class="layout--article"><!-- start custom article top snippet -->
|
|||
|
<style>
|
|||
|
#myBtn {
|
|||
|
display: none;
|
|||
|
position: fixed;
|
|||
|
bottom: 10px;
|
|||
|
right: 10px;
|
|||
|
z-index: 99;
|
|||
|
font-size: 12px;
|
|||
|
font-weight: bold;
|
|||
|
border: none;
|
|||
|
outline: none;
|
|||
|
background-color: white;
|
|||
|
color: black;
|
|||
|
cursor: pointer;
|
|||
|
padding: 5px;
|
|||
|
border-radius: 4px;
|
|||
|
}
|
|||
|
|
|||
|
#myBtn:hover {
|
|||
|
background-color: #555;
|
|||
|
}
|
|||
|
</style>
|
|||
|
|
|||
|
<button onclick="topFunction()" id="myBtn" title="Haut de page">⇧</button>
|
|||
|
|
|||
|
<script>
|
|||
|
//Get the button
|
|||
|
var mybutton = document.getElementById("myBtn");
|
|||
|
|
|||
|
// When the user scrolls down 20px from the top of the document, show the button
|
|||
|
window.onscroll = function() {scrollFunction()};
|
|||
|
|
|||
|
function scrollFunction() {
|
|||
|
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
|
|||
|
mybutton.style.display = "block";
|
|||
|
} else {
|
|||
|
mybutton.style.display = "none";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// When the user clicks on the button, scroll to the top of the document
|
|||
|
function topFunction() {
|
|||
|
document.body.scrollTop = 0;
|
|||
|
document.documentElement.scrollTop = 0;
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
|
|||
|
<!-- end custom article top snippet -->
|
|||
|
<div class="article__content" itemprop="articleBody"><details>
|
|||
|
<summary><b>Afficher/cacher Sommaire</b></summary>
|
|||
|
<!-- affichage sommaire -->
|
|||
|
<div class="toc-aside js-toc-root"></div>
|
|||
|
</details><p>Lorsque j’ai installé <a href="https://linuxfr.org/news/mon-nouveau-smartphone-android-degooglise">LineageOS sur mon smartphone</a>, j’ai suivi des tutoriels sans réellement comprendre ce que je faisais. Je voulais donc écrire cette dépêche pour éclairer celles et ceux qui voudraient tenter l’aventure et avoir plus de contrôle sur leur téléphone Android. Ce n’est donc pas un tutoriel mais plus un guide qui explique le fonctionnement et l’écosystème d’Android et de LineageOS.</p>
|
|||
|
|
|||
|
<p><img src="https://lineageos.org/assets/img/security.png" alt="copies d'écran LineageOS" /></p>
|
|||
|
|
|||
|
<hr />
|
|||
|
|
|||
|
<p><a href="https://www.lineageos.org/">LineageOS</a><br />
|
|||
|
<a href="https://f-droid.org/">F-Droid</a><br />
|
|||
|
<a href="https://microg.org/">MicroG</a><br />
|
|||
|
<a href="https://chatons.org/fr">CHATONS (cloud et synchronisation)</a><br />
|
|||
|
<a href="https://wiki.zaclys.com/index.php/D%C3%A9googliser_votre_t%C3%A9l%C3%A9phone_Android">Dégoogliser votre téléphone Android</a><br />
|
|||
|
<a href="http://www.montelephonelibre.fr/">Mon téléphone libre (site de l’auteur)</a></p>
|
|||
|
|
|||
|
<hr />
|
|||
|
|
|||
|
<h1 id="avant-linstallation">Avant l’installation</h1>
|
|||
|
|
|||
|
<h2 id="quest-ce-que-lineageos-">Qu’est-ce que LineageOS ?</h2>
|
|||
|
|
|||
|
<p>LineageOS est un système pour téléphone et tablettes basé sur Android Open Source Project (AOSP) — Android tel que fournit par Google. Il peut être installé à la place du système Android qui est préinstallé sur les téléphones. Contrairement à ce système, maintenu (ou pas) par le fabriquant de l’appareil, il s’agit d’un projet communautaire.
|
|||
|
C’est un dérivé de Cyanogenmod créé en décembre 2016, quand la boite qui était derrière ce dernier, Cyanogen Inc., a annoncé l’arrêt du projet et de son infrastructure.</p>
|
|||
|
|
|||
|
<h2 id="système-alternatif--quelle-différences-entre-un-téléphone-et-un-ordinateur-classique-">Système alternatif : quelle différences entre un téléphone et un ordinateur classique ?</h2>
|
|||
|
|
|||
|
<p>Lorsqu’on a déjà installé un ordinateur sous Linux ou avec un autre système, on se demande forcément à un moment ou à un autre : mais pourquoi est-ce si compliqué, qu’y a t-il de différent sur un téléphone android ?</p>
|
|||
|
|
|||
|
<p>Sur les ordinateurs de bureau ou portables classiques (PC), l’architecture est standardisée et le BIOS fournit la liste du matériel présent. Ainsi, un installateur de système d’exploitation pourra connaître et trouver le matériel sans problème particulier.</p>
|
|||
|
|
|||
|
<p>Pour les téléphones, c’est plus compliqué : l’architecture et le matériel sont souvent spécifiques pour chaque téléphone et il n’y a pas de moyen de détection. Le noyau des systèmes des téléphones doivent être compilés avec un arbre de périphérique (<strong>device tree</strong> : liste du matériel présent et à quel endroit il est).</p>
|
|||
|
|
|||
|
<p>Pour compliquer le tout, les constructeurs des cartes intégrées utilisées dans les téléphones adaptent en profondeur le noyau Linux et le rendent spécifique à leur matériel. Ce qui fait que chaque carte est fournie avec un noyau Linux spécifique et un ensemble de bibliothèques propriétaires pour faire fonctionner des périphériques plus ou moins essentiels (puce graphique, appareil photo, modem, puce wifi et bluetooth, etc.).</p>
|
|||
|
|
|||
|
<p><strong>Pour toutes ces raisons, il n’y a pas d’installateur universel, il y a autant d’images que de téléphones.</strong></p>
|
|||
|
|
|||
|
<h2 id="android-et-la-vie-privée--quest-ce-qui-pose-problème-">Android et la vie privée : qu’est ce qui pose problème ?</h2>
|
|||
|
|
|||
|
<p>Voici quelques liens qui peuvent donner envie de mieux protéger sa vie privée :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Infographie des <a href="https://e.foundation/wp-content/uploads/INFOGRAPHIE-V5.gif">données envoyées à Google</a>, en anglais.</li>
|
|||
|
<li>Achats, déplacements, enregistrements de voix… <a href="https://mobile.francetvinfo.fr/internet/google/achats-deplacements-enregistrements-de-voix-j-ai-fouille-dans-les-donnees-que-google-conserve-sur-moi-depuis-treize-ans-et-rien-ne-lui-echappe_3254425.html">J’ai fouillé dans les données que Google conserve sur moi depuis treize ans (et rien ne lui échappe)</a>.</li>
|
|||
|
<li>Documentaire <a href="https://vimeo.com/193515863">Nothing to hide</a> - VO sous-titré en français, on a parlé de la suite <a href="https://linuxfr.org/news/documentaire-disparaitre-suite-de-nothing-to-hide-en-crowdfunding-derniers-jours">sur LinuxFr.org</a>.</li>
|
|||
|
<li><a href="https://framablog.org/2019/01/12/les-donnees-que-recolte-google-document-complet/">Ce que collecte Google</a>, traduction par Framasoft de l’étude <a href="https://digitalcontentnext.org/wp-content/uploads/2018/08/DCN-Google-Data-Collection-Paper.pdf">Google Data Collection</a> de l’équipe du professeur Douglas C. Schmidt, spécialiste des systèmes logiciels, chercheur et enseignant à l’Université Vanderbilt.</li>
|
|||
|
<li>Bilan de sécurité 2018 Android analysé par <a href="https://www.nextinpact.com/news/107754-android-bilan-securite-2018-positif-mais-imparfait.htm">Next-inpact</a>.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="pourquoi-installer-lineageos-">Pourquoi installer LineageOS ?</h2>
|
|||
|
|
|||
|
<h3 id="avantages-">Avantages :</h3>
|
|||
|
<ul>
|
|||
|
<li>protéger sa vie privée ;</li>
|
|||
|
<li>avoir plus de contrôle sur son téléphone ;</li>
|
|||
|
<li>avoir un téléphone mis à jour avec une version récente d’Android ;</li>
|
|||
|
<li>gagner en vitesse d’exécution et en espace libre (pas de surcouche constructeur).</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h3 id="inconvénients-">Inconvénients :</h3>
|
|||
|
<ul>
|
|||
|
<li>perte de la garantie (bien que cette clause soit abusive, je vous laisse en discuter dans les commentaires) ;</li>
|
|||
|
<li>difficulté et risque de l’installation (possibilité de bloquer son téléphone en cas de mauvaise manipulation, cette page devrait vous aider à mieux comprendre ce que vous faites).</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="android-est-il-libre-">Android est-il libre ?</h2>
|
|||
|
<p>Oui et non, le système de base Android est libre (<strong>AOSP</strong> : Android Open Source Project) mais les téléphones sont livrés avec des applications et bibliothèques propriétaires.</p>
|
|||
|
|
|||
|
<p>Voici généralement ce que l’on trouve dans un téléphone livré par un constructeur :</p>
|
|||
|
|
|||
|
<p><img src="http://www.pixngraph.com/android-linuxfr/aosp-stock.png" alt="Rom Stock" /></p>
|
|||
|
|
|||
|
<p><strong>Google Apps</strong> : Applications fournies par Google (Play store, Gmail, Gmaps, etc.). Il est possible d’installer les <a href="https://en.wikipedia.org/wiki/List_of_Google_apps_for_Android">Google Apps sur LineageOS</a>.</p>
|
|||
|
|
|||
|
<p>Et voici ce que vous aurez après avoir installé LineageOS :</p>
|
|||
|
|
|||
|
<p><img src="http://www.pixngraph.com/android-linuxfr/aosp-lineageos.png" alt="Rom LineageOS" /></p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Les pilotes matériels restent non libres ;</li>
|
|||
|
<li>l’installation d’applications non libres reste possible sur LineageOS (oui, vous pourrez continuer à utiliser Facebook sur LineageOS si vous le désirez !) ;</li>
|
|||
|
<li>il est également possible d’installer les Google Apps sur LineageOS, mais vous vous exposerez alors à nouveau votre vie privée. Je reparlerai plus tard de l’alternative microG.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="quest-ce-quune-rom-">Qu’est ce qu’une ROM ?</h2>
|
|||
|
|
|||
|
<p>On rencontre souvent le terme « ROM » (Read-Only Memory) pour parler de LineageOS. C’est un peu un abus de langage ici car on devrait plutôt parler de distribution.
|
|||
|
Une ROM est installée sur un téléphone en « flashant » une image, c’est-à-dire en copiant son contenu dans la partition système du téléphone, qui est en lecture seule dans les conditions habituelles d’utilisation.
|
|||
|
Par <em>ROM stock</em>, on désigne le système qui est préinstallé sur le téléphone. Toutes les autres ROMs sont qualifiées de <em>ROM custom</em>. On parle aussi de temps en temps de <em>firmware</em> (micrologiciel), bien qu’une distribution Android n’est pas si micro : elles avoisinent le giga-octet.</p>
|
|||
|
|
|||
|
<h3 id="alternatives-">Alternatives :</h3>
|
|||
|
|
|||
|
<p>LineageOS est la ROMs alternative la plus répandue, pouvant s’installer sur le plus grand nombre de téléphones, mais il y en a d’autres :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>le projet <a href="https://replicant.us/">Replicant</a> a pour but de construire une version d’Android entièrement libre (y compris les pilotes) ;</li>
|
|||
|
<li><a href="https://e.foundation/">/e/</a> se base sur LineageOS pour proposer une autre interface et des services qui respectent la vie privée et intègre microG;</li>
|
|||
|
<li>ROMs basées sur LineageOS, telles que <a href="https://www.aicp-rom.com/">AICP</a>, <a href="https://blissroms.com/">Bliss</a> et un tas d’autres ; si vous arrivez à installer LineageOS, vous ne devriez pas rencontrer de problèmes pour tester d’autres ROMs basées dessus ;</li>
|
|||
|
<li><a href="https://source.android.com/">AOSP</a> elle-même, pour les téléphones pris en charge par Google ;</li>
|
|||
|
<li>des ROMs basées sur AOSP (et pas sur LineageOS), comme <a href="https://www.aospextended.com/">AOSPExtended</a>.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="est-ce-que-je-peux-installer-lineageos-sur-mon-téléphone-">Est-ce que je peux installer LineageOS sur mon téléphone ?</h2>
|
|||
|
|
|||
|
<p>Tous les téléphones ne sont pas compatibles avec LineageOS, <a href="https://download.lineageos.org/">voici la liste des téléphones officiellement compatibles</a>. Pour connaître le nom précis de votre appareil, allez sur <strong>Configuration</strong> - <strong>Nom de l’appareil</strong>.</p>
|
|||
|
|
|||
|
<p>Votre téléphone n’est pas dans la liste ? Tout n’est pas perdu, des passionnés développent des ROM non officielles de belle qualité. Vous pouvez rechercher s’il existe une ROM pour votre téléphone sur les <a href="https://forum.xda-developers.com">forums XDA</a>.</p>
|
|||
|
|
|||
|
<p>Exemple :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>j’ai un LG G5, le nom de l’appareil est H850 ;</li>
|
|||
|
<li>il n’est pas dans la liste officielle LineageOS ;</li>
|
|||
|
<li>il existe une <a href="https://forum.xda-developers.com/lg-g5/development/rom-unofficial-lineageos-15-1-g5-t3806842">ROM non officielle LineageOS 15.1</a> ;</li>
|
|||
|
<li>il existe une <a href="https://forum.xda-developers.com/lg-g5/development/rom-unofficial-lineageos-16-0-g5-t3917647">ROM Beta non officielle LineageOS 16</a>.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p>Cela peut paraître étrange de télécharger des fichiers d’installation à partir d’un forum, mais c’est comme cela dans le monde Android / LineageOS !</p>
|
|||
|
|
|||
|
<h3 id="les-versions-lineageos--android">Les versions LineageOS / Android</h3>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>CyanogenMod 12 (basé sur Android 5 - Lollipop)</li>
|
|||
|
<li>CyanogenMod 13 (basé sur Android 6 - Marshmallow)</li>
|
|||
|
<li>LineageOS 14 (basé sur Android 7 - Nougat)</li>
|
|||
|
<li>LineageOS 15 (basé sur Android 8 - Oréo)</li>
|
|||
|
<li>LineageOS 16 (basé sur Android 9 - Pixel)</li>
|
|||
|
<li>LineageOS 17 (basé sur Android 10 - Q)</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h1 id="linstallation">L’installation</h1>
|
|||
|
<p>Le but de cette partie est de comprendre ce que l’on fait lorsqu’on installe LineageOS, car bien souvent les tutoriels se limitent à une suite d’instructions à exécuter sans réelles explications.
|
|||
|
Je ne vais donc pas détailler ici l’installation de chaque ROM, les étapes diffèrent selon chaque téléphone. Donc, après la lecture de cet article, je vous renvoie donc au tutoriel d’installation de la ROM pour votre téléphone.</p>
|
|||
|
|
|||
|
<h2 id="le-système-de-fichiers">Le système de fichiers</h2>
|
|||
|
|
|||
|
<p>Voici à quoi ressemble le système de fichiers sur un téléphone Android (il existe d’autres partitions, mais vous n’avez pas besoins de les connaitre pour comprendre l’installation d’une ROM).</p>
|
|||
|
|
|||
|
<p><img src="http://www.pixngraph.com/android-linuxfr/filesystem1-description.png" alt="filesystem" /></p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Les parties <em>bootloader</em> et <em>fastboot</em> ne font pas vraiment partie du système de fichiers.</li>
|
|||
|
<li>Les partitions <code class="language-plaintext highlighter-rouge">/boot</code> et <code class="language-plaintext highlighter-rouge">/system</code> sont en lecture seule lorsque Android est démarré, c’est pourquoi il est impossible de supprimer les surcouches constructeurs et les Gapps d’un téléphone livré par les constructeurs.</li>
|
|||
|
<li>La partition <code class="language-plaintext highlighter-rouge">/data</code> contient les données et les applications, seule cette partition est accessible en écriture lorsque le téléphone est démarré.</li>
|
|||
|
<li>Il est possible d’accéder à la partition <code class="language-plaintext highlighter-rouge">/recovery</code> pour avoir accès en écriture aux autres partitions.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="que-sont-les-logiciels-adb-android-debug-bridge-et-fastboot-">Que sont les logiciels adb (Android Debug Bridge) et fastboot ?</h2>
|
|||
|
|
|||
|
<p>Il est parfois nécessaire d’installer ces logiciels sur votre PC, ils permettent d’envoyer des commandes à votre téléphone et d’accéder à des partitions protégées en écriture.</p>
|
|||
|
|
|||
|
<p><img src="http://www.pixngraph.com/android-linuxfr/filesystem1-adb+fastboot.png" alt="filesystem" /></p>
|
|||
|
|
|||
|
<p>Pour l’installation sous Linux, il suffit généralement d’installer un paquet pour votre distribution.</p>
|
|||
|
|
|||
|
<p>Exemple pour debian/ubuntu :</p>
|
|||
|
|
|||
|
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt-get <span class="nb">install </span>adb fastboot
|
|||
|
</code></pre></div></div>
|
|||
|
|
|||
|
<p>Pour pouvoir lancer des commandes adb, il faut que votre téléphone les accepte et soit en mode développeur. Pour cela, vous devez aller dans :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li><strong>Paramètres</strong> - <strong>À propos du téléphone</strong> ;</li>
|
|||
|
<li>puis vous devez taper 7 fois sur la ligne <strong>Numéro de build</strong> (oui, oui, ce n’est pas une blague :)) ;</li>
|
|||
|
<li>retournez dans <strong>Paramètres</strong> - <strong>{} Options pour les développeurs</strong> ;</li>
|
|||
|
<li>activez l’option <strong>Débogage Android</strong> (cette option est dans <strong>Paramètres</strong> ; - <strong>Système</strong> - <strong>{} Options pour les développeurs</strong> à partir d’Android 9 - Pie).</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p>Voici quelques exemples de ce que vous pouvez faire avec ces commandes :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li><strong>adb devices</strong> : liste les téléphones reliés par USB à votre PC et prêts à recevoir des commandes ;</li>
|
|||
|
<li><strong>adb reboot recovery</strong> : permet de redémarrer sur la partition <code class="language-plaintext highlighter-rouge">/recovery</code> ;</li>
|
|||
|
<li><strong>adb reboot bootloader</strong> : permet de redémarrer en mode <code class="language-plaintext highlighter-rouge">fastboot</code> ;</li>
|
|||
|
<li><strong>adb install -l nomapplication.apk</strong> : permet d’installer le <em>package</em> ;</li>
|
|||
|
<li><strong>fastboot devices</strong> : permet de lister les téléphones reliés par USB à votre PC et prêts à recevoir des commandes <em>fastboot</em>.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="pourquoi-déverrouiller-le-bootloader-">Pourquoi déverrouiller le bootloader ?</h2>
|
|||
|
|
|||
|
<p>Il faut déverrouiller le <em>bootloader</em> pour pouvoir démarrer sur la partition <code class="language-plaintext highlighter-rouge">/recovery</code>. La manipulation dépendra entièrement du constructeur et du téléphone que vous possédez. Cela peut se faire très simplement ou alors passer par une procédure complexe (envoi de mail, code, etc.).</p>
|
|||
|
|
|||
|
<p><img src="http://www.pixngraph.com/android-linuxfr/filesystem1-bootloader.png" alt="filesystem" /></p>
|
|||
|
|
|||
|
<h2 id="quest-ce-que-twrp-">Qu’est ce que TWRP ?</h2>
|
|||
|
|
|||
|
<p><a href="https://twrp.me/">TWRP</a> est un programme qui s’installe dans la partition <code class="language-plaintext highlighter-rouge">/recovery</code>. Une fois installé, il vous permet, grâce à une interface graphique, de formater, de sauvegarder ou d’installer des images dans les partitions <code class="language-plaintext highlighter-rouge">/boot</code> et <code class="language-plaintext highlighter-rouge">/system</code>.</p>
|
|||
|
|
|||
|
<p>Bien souvent, depuis la page d’installation de la ROM LineageOS, des liens vers le logiciel TWRP à flasher spécifiques à votre téléphone sont disponibles.</p>
|
|||
|
|
|||
|
<p><strong>Remarque</strong> : le projet TWRP propose des versions « live » permettant d’amorcer TWRP sans l’installer et d’y réaliser les opérations courantes (installation, injection d’une application système ou d’un patch).</p>
|
|||
|
|
|||
|
<p><img src="http://www.pixngraph.com/android-linuxfr/filesystem1-twrp.png" alt="filesystem" /></p>
|
|||
|
|
|||
|
<p>Pour démarrer sur TWRP, il faut utiliser une combinaison de touches qui dépend de votre téléphone (souvent bouton de démarrage + volume haut).</p>
|
|||
|
|
|||
|
<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fe/TWRP_2.7.0.0.png/300px-TWRP_2.7.0.0.png" alt="TWRP" /></p>
|
|||
|
|
|||
|
<p>TWRP est un logiciel libre, il possède une application permettant sa propre mise à jour simplifiée et aussi de se tenir informé des dernières versions du système.</p>
|
|||
|
|
|||
|
<h2 id="installation-de-lineageos">Installation de LineageOS</h2>
|
|||
|
<p>Et voilà, vous pouvez maintenant installer LineageOS : téléchargez la ROM, copiez-la sur une carte SD, rebootez sur <code class="language-plaintext highlighter-rouge">/recovery</code> en TWRP, <a href="https://www.stechguide.com/take-twrp-backup-directly-pc-via-adb/">sauvegardez votre ROM actuelle</a>, formatez les partitions, et enfin, installez votre nouvelle ROM !</p>
|
|||
|
|
|||
|
<p><img src="http://www.pixngraph.com/android-linuxfr/filesystem1-lineageos.png" alt="filesystem" /></p>
|
|||
|
|
|||
|
<h2 id="quest-ce-que-le-mode-root-">Quest-ce que le mode root ?</h2>
|
|||
|
<p>Après avoir démarré LineageOS, la partition <code class="language-plaintext highlighter-rouge">/system</code> n’est pas accessible en écriture. Rooter son téléphone permet de la rendre accessible.</p>
|
|||
|
|
|||
|
<p><img src="http://www.pixngraph.com/android-linuxfr/filesystem1-root.png" alt="filesystem" /></p>
|
|||
|
|
|||
|
<h3 id="avantages-du-root">Avantages du root :</h3>
|
|||
|
<ul>
|
|||
|
<li>Pouvoir sauvegarder l’ensemble de son appareil ;</li>
|
|||
|
<li>faire fonctionner des logiciels qui ont besoin d’accéder à la partition <code class="language-plaintext highlighter-rouge">/system</code> (MicroG ou logiciels de backup par exemple).</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h3 id="inconvénients-du-root">Inconvénients du root :</h3>
|
|||
|
<ul>
|
|||
|
<li>certaines applications ne fonctionnent plus sur un appareil rooté ;</li>
|
|||
|
<li>problèmes de sécurité ?</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h3 id="installation-de-magisk">Installation de Magisk</h3>
|
|||
|
<p>Magisk est un logiciel qui permet de faire un pseudo root et de cacher à certaines applications que le téléphone est rooté.
|
|||
|
<a href="https://magiskmanager.com">Magisk</a> est un logiciel libre, une application permet de le configurer et d’installer les dernière versions.</p>
|
|||
|
|
|||
|
<h2 id="quest-ce-que-microg-">Qu’est ce que MicroG ?</h2>
|
|||
|
|
|||
|
<p>Si vous voulez dégoogliser votre téléphone, vous ne voudrez certainement pas installer les Google Apps (GApps). Cependant, certaines applications dépendent des services Google.
|
|||
|
On pourrait penser que les <a href="https://opengapps.org/">Open Gapps</a> font l’affaire, mais ce n’est pas le cas : elles ne font que fournir des logiciels propriétaires de Google.
|
|||
|
On pourrait également penser que l’on peut tout simplement s’en passer. C’est effectivement la meilleure solution, mais, parfois, certaines applications dépendent des Google Apps et ne fonctionnent pas sans.</p>
|
|||
|
|
|||
|
<p><a href="https://microg.org/">MicroG</a> fournit une alternative libre à certaines API utilisées par ces applications et peut-être installé à la place des GApps. Il faut malgré tout rester conscient que le bon fonctionnement de microG dépend du bon vouloir de Google et de l’évolution de ses API.</p>
|
|||
|
|
|||
|
<p><img src="http://www.pixngraph.com/android-linuxfr/filesystem1-microg.png" alt="filesystem" /></p>
|
|||
|
|
|||
|
<p><strong>Remarque</strong> : En plus des ces API, MicroG fournit un moyen de se géolocaliser à l’aide du Wifi et des antennes téléphoniques, ce qui n’est pas fourni de base avec LineageOS (mais qui est une fonctionnalité des GApps).
|
|||
|
Cette fonctionnalité rend MicroG intéressant même pour les gens qui souhaitent totalement éviter Google car cette géolocalisation vient en complément et accélère parfois la localisation GPS.
|
|||
|
L’utilisateur devra choisir et activer des fournisseurs de géolocalisation dépendant ou non d’une connexion au réseau (Apple, Mozilla, bases de données locales) dans les paramètres de MicroG.</p>
|
|||
|
|
|||
|
<h3 id="rom-lineageos-for-microg">ROM LineageOS for microG</h3>
|
|||
|
|
|||
|
<p>Tous les téléphones pris officiellement en charge par LineageOS sont pris en charge par le projet <a href="https://lineage.microg.org/">LineageOS for microG</a>. Ces images fournissent LineageOS + microG + F-Droid de base. Cette solution simplifie grandement l’installation et doit être privilégiée.</p>
|
|||
|
|
|||
|
<h3 id="étapes-dinstallation-de-microg">Étapes d’installation de microG</h3>
|
|||
|
|
|||
|
<p>Si votre ROM n’est pas officiellement supportée, microG peut aussi être installé sur un système LineageOS existant, mais ce n’est pas simple.</p>
|
|||
|
|
|||
|
<p>Voici un résumé des étapes à réaliser pour l’installation de microG sans utiliser LineageOS for microG :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>rooter l’appareil ;</li>
|
|||
|
<li>ajouter un <a href="https://microg.org/download.html">dépôt à F-Droid</a> ;</li>
|
|||
|
<li>installer les applications microG (GmsCore, UnifiedNlp, FakeStore, etc) ;</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p><strong>Remarque</strong> : Une application permet de savoir quels services fonctionnent et de configurer microG.</p>
|
|||
|
|
|||
|
<h2 id="mettre-à-jour-sa-rom">Mettre à jour sa ROM</h2>
|
|||
|
<p>Si vous utilisez une ROM LineageOS (ou MicroG) officielle, vous devriez être notifié lorsqu’une mise à jour est disponible. L’installation (OTA : Over The Air) se fait donc très simplement.</p>
|
|||
|
|
|||
|
<p>Par contre, si vous êtes sur une version non officielle, je n’ai pas trouvé de méthode recommandée pour cette tâche. Je vous soumets donc la méthode que j’utilise et qui a fonctionné pour moi jusqu’à aujourd’hui. N’hésitez pas à en soumettre d’autres ou à la critiquer.</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Téléchargez la nouvelle ROM et placez là sur la carte SD ;</li>
|
|||
|
<li>Rebootez en mode recovery sous TWRP ;</li>
|
|||
|
<li>Installez la nouvelle ROM ;</li>
|
|||
|
<li>Redémarrez l’appareil. Le premier démarrage est en général long.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h1 id="après-linstallation">Après l’installation</h1>
|
|||
|
|
|||
|
<p>LineageOS est vraiment un système agréable à utiliser et complet, mais voici quelques suggestions de ce que vous pourriez faire après avoir installé LineageOS pour l’enrichir et le personnaliser.</p>
|
|||
|
|
|||
|
<h2 id="les-magasins-dapplications-f-droid---aurora">Les magasins d’applications (F-Droid - Aurora)</h2>
|
|||
|
<p>Que serait Android sans ses applications ? Et, pour installer des applications, il faut un magasin d’applications (Store). Si vous n’avez pas installé les Google Apps, vous n’avez plus Google Play, mais heureusement, des solutions alternatives existent.</p>
|
|||
|
|
|||
|
<h3 id="f-droid---magasin-dapplications-libres">F-droid - Magasin d’applications libres</h3>
|
|||
|
<p><a href="https://f-droid.org/fr/">F-droid</a> est un magasin d’applications libres pour Android. Pour l’installer, vous devez utiliser un navigateur, télécharger l’APK et l’installer.
|
|||
|
<img src="https://f-droid.org/repo/org.fdroid.fdroid/en-US/phoneScreenshots/screenshot-dark-home.png" alt="f-droid" /></p>
|
|||
|
|
|||
|
<h3 id="aurora---utiliser-google-play-sans-google-play">Aurora - Utiliser Google Play sans Google Play</h3>
|
|||
|
<p><a href="https://f-droid.org/fr/packages/com.aurora.store/">Aurora</a> (disponible sur f-droid) permet d’installer des applications provenant de Google Play sans utiliser de compte Google. Vous pouvez donc installer des applications propriétaires de façon anonyme grâce à ce store.</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Cela ne fonctionne pas toujours du premier coup, il faut parfois patienter avant de pouvoir installer une application (renouvellement de Token) ;</li>
|
|||
|
<li>Aurora vous demande un compte Google au démarrage, mais vous pouvez ignorer cette étape pour rester anonyme</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="quelques-suggestions-dapplications-libres">Quelques suggestions d’applications libres</h2>
|
|||
|
|
|||
|
<h3 id="navigation-web--firefox-et-duckduckgo">Navigation web : Firefox et DuckDuckGo</h3>
|
|||
|
<p>LineageOS est livré avec le navigateur Android. Mais, depuis F-droid, vous pouvez facilement installer le navigateur <a href="https://f-droid.org/fr/packages/com.duckduckgo.mobile.android/">DuckDuckGo</a>, qui propose également un widget de recherche ou alors Firefox (baptisé ici <a href="https://f-droid.org/fr/packages/org.mozilla.fennec_fdroid/">Fennec F-Droid</a>).
|
|||
|
Les plus puristes seront peut-être intéressés par <a href="https://f-droid.org/en/packages/org.gnu.icecat">Icecat Mobile</a>, un dérivé des versions Firefox prises en charge à long terme fourni avec les extensions LibreJS (qui bloque les scripts non-libres) et Tor.</p>
|
|||
|
|
|||
|
<h3 id="cartographie-et-navigation--osmand-et-mapsme">Cartographie et navigation : OsmAnd et Maps.Me</h3>
|
|||
|
|
|||
|
<p>Il existe beaucoup d’alternatives de qualité basées sur OpenStreetMap pour remplacer Google Maps.
|
|||
|
La plus connue est <a href="https://f-droid.org/fr/packages/net.osmand.plus/">OsmAnd</a>, qui est un vrai couteau suisse de la navigation mobile et permet une navigation, des recherches et des calculs d’itinéraires (vélo, piéton, voiture, transports en commun) hors-lignes.
|
|||
|
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Osmand_street_routing.png/640px-Osmand_street_routing.png" alt="OsmAnd" />
|
|||
|
Une application plus rapide et plus légère (utilisant des cartes non-vectorielles), <a href="https://f-droid.org/en/packages/com.github.axet.maps">Maps.me</a>, pourra également intéresser certaines personnes.</p>
|
|||
|
|
|||
|
<h3 id="photographies--opencamera---freedcam---camera-roll">Photographies : OpenCamera - FreeDCam - Camera Roll</h3>
|
|||
|
<p>LineageOS est livrée avec une application pour prendre des photos. Mais celle-ci fera peut-être pâle figure par rapport à celle livrée par le constructeur et qu’il est maintenant impossible d’installer.</p>
|
|||
|
|
|||
|
<p><a href="https://f-droid.org/fr/packages/net.sourceforge.opencamera/">OpenCamera</a> sera sans doute une alternative plus complète.
|
|||
|
Essayez également <a href="https://f-droid.org/fr/packages/troop.com.freedcam/">FreeDCam</a> qui propose encore plus de fonctionnalités (pas sûr qu’elle fonctionne pour autant de téléphones qu’OpenCamera) mais qui est également plus complexe.
|
|||
|
<img src="http://www.pixngraph.com/android-linuxfr/freedcam.png" alt="freedcam" /></p>
|
|||
|
|
|||
|
<p>Enfin, pour organiser vos photos, testez <a href="https://f-droid.org/en/packages/us.koller.cameraroll/">Camera Roll</a> !</p>
|
|||
|
|
|||
|
<h3 id="gestion-des-fichiers--amaze">Gestion des fichiers : Amaze</h3>
|
|||
|
<p><a href="https://f-droid.org/fr/packages/com.amaze.filemanager/">Amaze</a> est un gestionnaire de fichiers complet (accès à des serveurs SMB, FTP et SFTP).</p>
|
|||
|
|
|||
|
<p><img src="https://fscl01.fonpit.de/userfiles/6898953/image/comment-degoogliser-android-amaze-images-00-w782.jpg" alt="Amaze" /></p>
|
|||
|
|
|||
|
<h3 id="gestion-des-mails--k9mail">Gestion des mails : K9mail</h3>
|
|||
|
<p><a href="https://f-droid.org/en/packages/com.fsck.k9/">K9mail</a> est une application mail éprouvée. Elle peut remplacer E-mail Android mais également Gmail.</p>
|
|||
|
|
|||
|
<h3 id="calendrier-et-contacts--davx-nextcloud-etar-et-opentasks">Calendrier et contacts : DAVx⁵, Nextcloud, Etar et OpenTasks</h3>
|
|||
|
|
|||
|
<p>Un <a href="https://dutailly.net/synchroniser-son-agenda-entre-thunderbird-sous-linux-et-android">tutoriel</a> très complet a été rédigé par <a href="https://linuxfr.org/news/synchronisation-thunderbird-android">Ysabeau</a> pour synchroniser calendrier et contact avec <a href="https://f-droid.org/fr/packages/at.bitfire.davdroid/">Davx^5</a> (anciennement DavDroid).</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Une application pour la gestion des <strong>contacts</strong> est livrée avec LineageOS.</li>
|
|||
|
<li>Pour le <strong>calendrier</strong>, on pourra utiliser Agenda, livré avec LineageOS ou <a href="https://f-droid.org/en/packages/ws.xsoh.etar/">Etar</a></li>
|
|||
|
<li><a href="https://play.google.com/store/apps/details?id=org.dmfs.tasks&hl=fr&showAllReviews=true">OpenTasks</a> - non disponible sur F-Droid pourra gérer et synchroniser vos <strong>TODO List</strong></li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h3 id="sauvegardes--oandbackup-sms-backup--et-nextcloud">Sauvegardes : OandBackup, SMS Backup + et Nextcloud</h3>
|
|||
|
|
|||
|
<p>L’application <a href="https://f-droid.org/en/packages/dk.jens.backup/">OandBackup</a> permet une sauvegarde exhaustive, par application, du téléphone. Même si son interface est un peu austère, vous pouvez tout sauvegarder avec, à condition d’avoir un téléphone rooté.
|
|||
|
<img src="https://framalibre.org/sites/default/files/OandbackupScreen1.jpg" alt="Oandbackup" /></p>
|
|||
|
|
|||
|
<p>L’application <a href="https://f-droid.org/fr/packages/com.zegoggles.smssync/">SMS Backup+</a> pourra être utilisée en complément d’OandBackup pour sauvegarder ses textos dans une boite mail. Elle ne nécessite pas un téléphone rooté.</p>
|
|||
|
|
|||
|
<p>Côté photos et documents, <a href="https://f-droid.org/en/packages/com.nextcloud.client">Nextcloud</a> ou <a href="https://f-droid.org/fr/packages/com.owncloud.android/">OwnCloud</a>, qui permettent d’accéder à un espace en ligne (cloud) et d’y téléverser les photos qui sont prises dès que le téléphone est connecté (c’est bien sûr optionnel).</p>
|
|||
|
|
|||
|
<p>Enfin, il est toujours possible de sauvegarder entièrement le téléphone, même s’il n’est pas rooté, <a href="https://www.stechguide.com/take-twrp-backup-directly-pc-via-adb/">à l’aide de TWRP</a>.</p>
|
|||
|
|
|||
|
<h3 id="et-puis-en-vrac-">Et puis, en vrac :</h3>
|
|||
|
<ul>
|
|||
|
<li><a href="https://f-droid.org/fr/packages/org.videolan.vlc/">VLC</a> : regarder des vidéos et écouter de la musique ;</li>
|
|||
|
<li><a href="https://f-droid.org/fr/packages/com.jmstudios.redmoon/">Red Moon</a> ou <a href="https://f-droid.org/en/packages/com.corphish.nightlight.generic/">Night light</a> : filtre les lumières bleues pour le bien de vos yeux ;</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/org.kde.kdeconnect_tp">KDE Connect</a> : permet de recevoir et répondre à ses textos avec son ordinateur, de partager le presse papier, transférer des fichiers, partager les notifications dans un sens comme dans l’autre, piloter son ordinateur à partir du téléphone (souris, clavier, lecteur de musique), ouvrir un lien sur le téléphone depuis l’ordinateur et plus encore : une application très pratique et assez complète qui fonctionne aussi sur d’autres environnement de bureau que Plasma ;</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/org.schabi.newpipe/">NewPipe</a> : une belle alternative à l’application Youtube, qui permet de chercher, visionner les vidéos, les écouter en arrière plan et les télécharger pour les regarder ou les écouter plus tard. Tout ça sans les publicités. Indisponible sur le Play Store ;</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/dev.ukanth.ufirewall">AfWall+</a> : un par-feu permettant un contrôle fin par application et type de connexion (pratique notamment pour limiter l’utilisation des données mobiles sur un forfait restreint ! Nécessite un téléphone rooté) ;</li>
|
|||
|
<li><a href="https://f-droid.org/fr/packages ;/com.catchingnow.tinyclipboardmanager/">ClipStack</a> : un bon gestionnaire de presse-papier.</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/com.suyashsrijan.forcedoze">ForceDoze</a> : pour forcer votre téléphone à dormir quand son écran est éteint. À utiliser avec précautions ;</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/com.jarsilio.android.drowser">Drowser</a> : pour tuer les applications voulues lorsque l’écran du téléphone s’éteint (idem !) ;</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/com.google.zxing.client.android">Barcode Scanner</a> : pour scanner les codes barres et partager des données avec des QR codes ;</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/openfoodfacts.github.scrachx.openfood">OpenFoodFacts</a> et <a href="https://f-droid.org/es/packages/openfoodfacts.github.scrachx.openbeauty">OpenBeautyFacts</a> : pour scanner et évaluer des produits du commerce avec la base de données <a href="https://world.openfoodfacts.org/">Open Food Facts</a> ;</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/com.google.android.stardroid">Sky Map</a> : une application anciennement éditée par Google et maintenant gérée par la communauté pour observer le ciel ;</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/com.vonglasow.michael.satstat">SatStat</a> : pour consulter la boussole du téléphone, afficher les données GPS et de divers capteurs du téléphone (accélération, rotation, champs magnétique, orientation, lumière, proximité, température, pression) ;</li>
|
|||
|
<li><a href="https://f-droid.org/wiki/page/com.simplemobiletools.draw">Dessin</a> : une application de dessin rudimentaire. Pratique pour noter les scores pendant une partie de jeu de société ;</li>
|
|||
|
<li><a href="https://f-droid.org/en/packages/com.github.axet.audiorecorder/">Audio Recorder</a> : enregistrer du son avec le microphone du téléphone ;</li>
|
|||
|
<li><a href="https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&hl=fr">Signal</a> - non disponible sur F-Droid (le développeur principal s’y oppose, est hostile aux dérivés se connectant au serveur officiel et l’application inclut une bibliothèque Google non-libre) : permet d’échanger des messages et des médias chiffrer avec ses contacts. Voir aussi : <a href="https://f-droid.org/en/packages/im.vector.alpha">Riot.im</a> (Matrix), <a href="https://f-droid.org/en/packages/chat.rocket.android">Rocket Chat</a>, <a href="https://f-droid.org/en/packages/com.mattermost.mattermost">Mattermost</a>, <a href="https://f-droid.org/en/packages/com.b44t.messenger">Delta Chat</a> (messagerie instantanée par courriels) et <a href="https://f-droid.org/en/packages/org.telegram.messenger">Telegram</a>.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h1 id="conclusion-et-projets">Conclusion et projets</h1>
|
|||
|
|
|||
|
<p>J’ai beaucoup appris en rédigeant cette dépêche. Merci également à toutes les personnes qui y ont participé.</p>
|
|||
|
|
|||
|
<p>La liste des applications présentées dans cette dépêche est loin d’être complète, je compte sur vous pour partager vos trouvailles dans les commentaires.</p>
|
|||
|
|
|||
|
<p>J’ai pour projet de faire quelques diapos à partir de cette dépêche dans le but d’organiser des « Expositions » sur Android et la vie privée. Beaucoup de personnes y sont aujourd’hui sensibles (même si beaucoup ne le sont pas également ;)).</p>
|
|||
|
|
|||
|
<p>Pour finir, j’aimerais également étudier la possibilité d’importer des téléphones reconditionnés, de les installer avec LineageOS et de les vendre en France avec une garantie. Cela m’a été demandé par des proches, et j’ai créé le site web <a href="http://www.montelephonelibre.fr/">http://www.montelephonelibre.fr</a> (soyez indulgents, le site a été créé pour cette dépêche, il est tout neuf :)). N’hésitez pas à m’envoyer des messages et à vous inscrire à la newsletter pour avoir des nouvelles !</p>
|
|||
|
|
|||
|
</div>
|
|||
|
|
|||
|
|
|||
|
|
|||
|
<div class="d-print-none"><footer class="article__footer"><meta itemprop="dateModified" content="2019-07-06T00:00:00+02:00"><!-- start custom article footer snippet -->
|
|||
|
|
|||
|
<!-- end custom article footer snippet -->
|
|||
|
<!--
|
|||
|
<div align="right"><a type="application/rss+xml" href="/feed.xml" title="S'abonner"><i class="fa fa-rss fa-2x"></i></a>
|
|||
|
|
|||
|
 </div>
|
|||
|
-->
|
|||
|
</footer>
|
|||
|
<div class="article__section-navigator clearfix"><div class="previous"><span>PRÉCÉDENT</span><a href="/2019/06/25/VirtualBox-Virtualiser-une-installation-physique-d_un-disque-hdd-ssd-usb.html">VirtualBox - Virtualiser une installation physique d’un système d’exploitation</a></div><div class="next"><span>SUIVANT</span><a href="/2019/07/06/Python-environnements-virtuels-VENV-et-Wing_Personnal_Python_IDE.html">Python - Création d'environnements virtuels VENV et "Wing Personnal" ,installer applis avec "pip"</a></div></div></div>
|
|||
|
|
|||
|
</div>
|
|||
|
|
|||
|
<script>(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
$(function() {
|
|||
|
var $this ,$scroll;
|
|||
|
var $articleContent = $('.js-article-content');
|
|||
|
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
|
|||
|
var scroll = hasSidebar ? '.js-page-main' : 'html, body';
|
|||
|
$scroll = $(scroll);
|
|||
|
|
|||
|
$articleContent.find('.highlight').each(function() {
|
|||
|
$this = $(this);
|
|||
|
$this.attr('data-lang', $this.find('code').attr('data-lang'));
|
|||
|
});
|
|||
|
$articleContent.find('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]').each(function() {
|
|||
|
$this = $(this);
|
|||
|
$this.append($('<a class="anchor d-print-none" aria-hidden="true"></a>').html('<i class="fas fa-anchor"></i>'));
|
|||
|
});
|
|||
|
$articleContent.on('click', '.anchor', function() {
|
|||
|
$scroll.scrollToAnchor('#' + $(this).parent().attr('id'), 400);
|
|||
|
});
|
|||
|
});
|
|||
|
});
|
|||
|
})();
|
|||
|
</script>
|
|||
|
|
|||
|
</div><section class="page__comments d-print-none"></section></article><!-- start custom main bottom snippet -->
|
|||
|
<!-- end custom main bottom snippet -->
|
|||
|
</div>
|
|||
|
</div></div></div></div>
|
|||
|
</div><script>(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
var $body = $('body'), $window = $(window);
|
|||
|
var $pageRoot = $('.js-page-root'), $pageMain = $('.js-page-main');
|
|||
|
var activeCount = 0;
|
|||
|
function modal(options) {
|
|||
|
var $root = this, visible, onChange, hideWhenWindowScroll = false;
|
|||
|
var scrollTop;
|
|||
|
function setOptions(options) {
|
|||
|
var _options = options || {};
|
|||
|
visible = _options.initialVisible === undefined ? false : show;
|
|||
|
onChange = _options.onChange;
|
|||
|
hideWhenWindowScroll = _options.hideWhenWindowScroll;
|
|||
|
}
|
|||
|
function init() {
|
|||
|
setState(visible);
|
|||
|
}
|
|||
|
function setState(isShow) {
|
|||
|
if (isShow === visible) {
|
|||
|
return;
|
|||
|
}
|
|||
|
visible = isShow;
|
|||
|
if (visible) {
|
|||
|
activeCount++;
|
|||
|
scrollTop = $(window).scrollTop() || $pageMain.scrollTop();
|
|||
|
$root.addClass('modal--show');
|
|||
|
$pageMain.scrollTop(scrollTop);
|
|||
|
activeCount === 1 && ($pageRoot.addClass('show-modal'), $body.addClass('of-hidden'));
|
|||
|
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.on('scroll', hide);
|
|||
|
$window.on('keyup', handleKeyup);
|
|||
|
} else {
|
|||
|
activeCount > 0 && activeCount--;
|
|||
|
$root.removeClass('modal--show');
|
|||
|
$window.scrollTop(scrollTop);
|
|||
|
activeCount === 0 && ($pageRoot.removeClass('show-modal'), $body.removeClass('of-hidden'));
|
|||
|
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.off('scroll', hide);
|
|||
|
$window.off('keyup', handleKeyup);
|
|||
|
}
|
|||
|
onChange && onChange(visible);
|
|||
|
}
|
|||
|
function show() {
|
|||
|
setState(true);
|
|||
|
}
|
|||
|
function hide() {
|
|||
|
setState(false);
|
|||
|
}
|
|||
|
function handleKeyup(e) {
|
|||
|
// Char Code: 27 ESC
|
|||
|
if (e.which === 27) {
|
|||
|
hide();
|
|||
|
}
|
|||
|
}
|
|||
|
setOptions(options);
|
|||
|
init();
|
|||
|
return {
|
|||
|
show: show,
|
|||
|
hide: hide,
|
|||
|
$el: $root
|
|||
|
};
|
|||
|
}
|
|||
|
$.fn.modal = modal;
|
|||
|
});
|
|||
|
})();
|
|||
|
</script><div class="modal modal--overflow page__search-modal d-print-none js-page-search-modal"><script>
|
|||
|
(function () {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
// search panel
|
|||
|
var search = (window.search || (window.search = {}));
|
|||
|
var useDefaultSearchBox = window.useDefaultSearchBox === undefined ?
|
|||
|
true : window.useDefaultSearchBox ;
|
|||
|
|
|||
|
var $searchModal = $('.js-page-search-modal');
|
|||
|
var $searchToggle = $('.js-search-toggle');
|
|||
|
var searchModal = $searchModal.modal({ onChange: handleModalChange, hideWhenWindowScroll: true });
|
|||
|
var modalVisible = false;
|
|||
|
search.searchModal = searchModal;
|
|||
|
|
|||
|
var $searchBox = null;
|
|||
|
var $searchInput = null;
|
|||
|
var $searchClear = null;
|
|||
|
|
|||
|
function getModalVisible() {
|
|||
|
return modalVisible;
|
|||
|
}
|
|||
|
search.getModalVisible = getModalVisible;
|
|||
|
|
|||
|
function handleModalChange(visible) {
|
|||
|
modalVisible = visible;
|
|||
|
if (visible) {
|
|||
|
search.onShow && search.onShow();
|
|||
|
useDefaultSearchBox && $searchInput[0] && $searchInput[0].focus();
|
|||
|
} else {
|
|||
|
search.onShow && search.onHide();
|
|||
|
useDefaultSearchBox && $searchInput[0] && $searchInput[0].blur();
|
|||
|
setTimeout(function() {
|
|||
|
useDefaultSearchBox && ($searchInput.val(''), $searchBox.removeClass('not-empty'));
|
|||
|
search.clear && search.clear();
|
|||
|
window.pageAsideAffix && window.pageAsideAffix.refresh();
|
|||
|
}, 400);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$searchToggle.on('click', function() {
|
|||
|
modalVisible ? searchModal.hide() : searchModal.show();
|
|||
|
});
|
|||
|
// Char Code: 83 S, 191 /
|
|||
|
$(window).on('keyup', function(e) {
|
|||
|
if (!modalVisible && !window.isFormElement(e.target || e.srcElement) && (e.which === 83 || e.which === 191)) {
|
|||
|
modalVisible || searchModal.show();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
if (useDefaultSearchBox) {
|
|||
|
$searchBox = $('.js-search-box');
|
|||
|
$searchInput = $searchBox.children('input');
|
|||
|
$searchClear = $searchBox.children('.js-icon-clear');
|
|||
|
search.getSearchInput = function() {
|
|||
|
return $searchInput.get(0);
|
|||
|
};
|
|||
|
search.getVal = function() {
|
|||
|
return $searchInput.val();
|
|||
|
};
|
|||
|
search.setVal = function(val) {
|
|||
|
$searchInput.val(val);
|
|||
|
};
|
|||
|
|
|||
|
$searchInput.on('focus', function() {
|
|||
|
$(this).addClass('focus');
|
|||
|
});
|
|||
|
$searchInput.on('blur', function() {
|
|||
|
$(this).removeClass('focus');
|
|||
|
});
|
|||
|
$searchInput.on('input', window.throttle(function() {
|
|||
|
var val = $(this).val();
|
|||
|
if (val === '' || typeof val !== 'string') {
|
|||
|
search.clear && search.clear();
|
|||
|
} else {
|
|||
|
$searchBox.addClass('not-empty');
|
|||
|
search.onInputNotEmpty && search.onInputNotEmpty(val);
|
|||
|
}
|
|||
|
}, 400));
|
|||
|
$searchClear.on('click', function() {
|
|||
|
$searchInput.val(''); $searchBox.removeClass('not-empty');
|
|||
|
search.clear && search.clear();
|
|||
|
});
|
|||
|
}
|
|||
|
});
|
|||
|
})();
|
|||
|
</script><div class="search search--dark">
|
|||
|
<div class="main">
|
|||
|
<div class="search__header">Recherche</div>
|
|||
|
<div class="search-bar">
|
|||
|
<div class="search-box js-search-box">
|
|||
|
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
|
|||
|
<input id="search-input" type="text" />
|
|||
|
<div class="search-box__icon-clear js-icon-clear">
|
|||
|
<a><i class="fas fa-times"></i></a>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<button class="button button--theme-dark button--pill search__cancel js-search-toggle">
|
|||
|
Annuler</button>
|
|||
|
</div>
|
|||
|
<div id="results-container" class="search-result js-search-result"></div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<!-- Script pointing to search-script.js -->
|
|||
|
<script>/*!
|
|||
|
* Simple-Jekyll-Search
|
|||
|
* Copyright 2015-2020, Christian Fei
|
|||
|
* Licensed under the MIT License.
|
|||
|
*/
|
|||
|
|
|||
|
(function(){
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Templater_7 = {
|
|||
|
compile: compile,
|
|||
|
setOptions: setOptions
|
|||
|
}
|
|||
|
|
|||
|
const options = {}
|
|||
|
options.pattern = /\{(.*?)\}/g
|
|||
|
options.template = ''
|
|||
|
options.middleware = function () {}
|
|||
|
|
|||
|
function setOptions (_options) {
|
|||
|
options.pattern = _options.pattern || options.pattern
|
|||
|
options.template = _options.template || options.template
|
|||
|
if (typeof _options.middleware === 'function') {
|
|||
|
options.middleware = _options.middleware
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function compile (data) {
|
|||
|
return options.template.replace(options.pattern, function (match, prop) {
|
|||
|
const value = options.middleware(prop, data[prop], options.template)
|
|||
|
if (typeof value !== 'undefined') {
|
|||
|
return value
|
|||
|
}
|
|||
|
return data[prop] || match
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
'use strict';
|
|||
|
|
|||
|
function fuzzysearch (needle, haystack) {
|
|||
|
var tlen = haystack.length;
|
|||
|
var qlen = needle.length;
|
|||
|
if (qlen > tlen) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (qlen === tlen) {
|
|||
|
return needle === haystack;
|
|||
|
}
|
|||
|
outer: for (var i = 0, j = 0; i < qlen; i++) {
|
|||
|
var nch = needle.charCodeAt(i);
|
|||
|
while (j < tlen) {
|
|||
|
if (haystack.charCodeAt(j++) === nch) {
|
|||
|
continue outer;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
var _$fuzzysearch_1 = fuzzysearch;
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
|
|||
|
|
|||
|
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
|
|||
|
|
|||
|
function FuzzySearchStrategy () {
|
|||
|
this.matches = function (string, crit) {
|
|||
|
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
|
|||
|
|
|||
|
function LiteralSearchStrategy () {
|
|||
|
this.matches = function (str, crit) {
|
|||
|
if (!str) return false
|
|||
|
|
|||
|
str = str.trim().toLowerCase()
|
|||
|
crit = crit.trim().toLowerCase()
|
|||
|
|
|||
|
return crit.split(' ').filter(function (word) {
|
|||
|
return str.indexOf(word) >= 0
|
|||
|
}).length === crit.split(' ').length
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Repository_4 = {
|
|||
|
put: put,
|
|||
|
clear: clear,
|
|||
|
search: search,
|
|||
|
setOptions: __setOptions_4
|
|||
|
}
|
|||
|
|
|||
|
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
|
|||
|
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
|
|||
|
|
|||
|
function NoSort () {
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
const data = []
|
|||
|
let opt = {}
|
|||
|
|
|||
|
opt.fuzzy = false
|
|||
|
opt.limit = 10
|
|||
|
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = NoSort
|
|||
|
opt.exclude = []
|
|||
|
|
|||
|
function put (data) {
|
|||
|
if (isObject(data)) {
|
|||
|
return addObject(data)
|
|||
|
}
|
|||
|
if (isArray(data)) {
|
|||
|
return addArray(data)
|
|||
|
}
|
|||
|
return undefined
|
|||
|
}
|
|||
|
function clear () {
|
|||
|
data.length = 0
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function isObject (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
|
|||
|
}
|
|||
|
|
|||
|
function isArray (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
|
|||
|
}
|
|||
|
|
|||
|
function addObject (_data) {
|
|||
|
data.push(_data)
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function addArray (_data) {
|
|||
|
const added = []
|
|||
|
clear()
|
|||
|
for (let i = 0, len = _data.length; i < len; i++) {
|
|||
|
if (isObject(_data[i])) {
|
|||
|
added.push(addObject(_data[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
return added
|
|||
|
}
|
|||
|
|
|||
|
function search (crit) {
|
|||
|
if (!crit) {
|
|||
|
return []
|
|||
|
}
|
|||
|
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
|
|||
|
}
|
|||
|
|
|||
|
function __setOptions_4 (_opt) {
|
|||
|
opt = _opt || {}
|
|||
|
|
|||
|
opt.fuzzy = _opt.fuzzy || false
|
|||
|
opt.limit = _opt.limit || 10
|
|||
|
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = _opt.sort || NoSort
|
|||
|
opt.exclude = _opt.exclude || []
|
|||
|
}
|
|||
|
|
|||
|
function findMatches (data, crit, strategy, opt) {
|
|||
|
const matches = []
|
|||
|
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
|
|||
|
const match = findMatchesInObject(data[i], crit, strategy, opt)
|
|||
|
if (match) {
|
|||
|
matches.push(match)
|
|||
|
}
|
|||
|
}
|
|||
|
return matches
|
|||
|
}
|
|||
|
|
|||
|
function findMatchesInObject (obj, crit, strategy, opt) {
|
|||
|
for (const key in obj) {
|
|||
|
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
|
|||
|
return obj
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isExcluded (term, excludedTerms) {
|
|||
|
for (let i = 0, len = excludedTerms.length; i < len; i++) {
|
|||
|
const excludedTerm = excludedTerms[i]
|
|||
|
if (new RegExp(excludedTerm).test(term)) {
|
|||
|
return true
|
|||
|
}
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
|
|||
|
/* globals ActiveXObject:false */
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$JSONLoader_2 = {
|
|||
|
load: load
|
|||
|
}
|
|||
|
|
|||
|
function load (location, callback) {
|
|||
|
const xhr = getXHR()
|
|||
|
xhr.open('GET', location, true)
|
|||
|
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
|
|||
|
xhr.send()
|
|||
|
}
|
|||
|
|
|||
|
function createStateChangeListener (xhr, callback) {
|
|||
|
return function () {
|
|||
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|||
|
try {
|
|||
|
callback(null, JSON.parse(xhr.responseText))
|
|||
|
} catch (err) {
|
|||
|
callback(err, null)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function getXHR () {
|
|||
|
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$OptionsValidator_3 = function OptionsValidator (params) {
|
|||
|
if (!validateParams(params)) {
|
|||
|
throw new Error('-- OptionsValidator: required options missing')
|
|||
|
}
|
|||
|
|
|||
|
if (!(this instanceof OptionsValidator)) {
|
|||
|
return new OptionsValidator(params)
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = params.required
|
|||
|
|
|||
|
this.getRequiredOptions = function () {
|
|||
|
return requiredOptions
|
|||
|
}
|
|||
|
|
|||
|
this.validate = function (parameters) {
|
|||
|
const errors = []
|
|||
|
requiredOptions.forEach(function (requiredOptionName) {
|
|||
|
if (typeof parameters[requiredOptionName] === 'undefined') {
|
|||
|
errors.push(requiredOptionName)
|
|||
|
}
|
|||
|
})
|
|||
|
return errors
|
|||
|
}
|
|||
|
|
|||
|
function validateParams (params) {
|
|||
|
if (!params) {
|
|||
|
return false
|
|||
|
}
|
|||
|
return typeof params.required !== 'undefined' && params.required instanceof Array
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$utils_9 = {
|
|||
|
merge: merge,
|
|||
|
isJSON: isJSON
|
|||
|
}
|
|||
|
|
|||
|
function merge (defaultParams, mergeParams) {
|
|||
|
const mergedOptions = {}
|
|||
|
for (const option in defaultParams) {
|
|||
|
mergedOptions[option] = defaultParams[option]
|
|||
|
if (typeof mergeParams[option] !== 'undefined') {
|
|||
|
mergedOptions[option] = mergeParams[option]
|
|||
|
}
|
|||
|
}
|
|||
|
return mergedOptions
|
|||
|
}
|
|||
|
|
|||
|
function isJSON (json) {
|
|||
|
try {
|
|||
|
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
|
|||
|
return true
|
|||
|
}
|
|||
|
return false
|
|||
|
} catch (err) {
|
|||
|
return false
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var _$src_8 = {};
|
|||
|
(function (window) {
|
|||
|
'use strict'
|
|||
|
|
|||
|
let options = {
|
|||
|
searchInput: null,
|
|||
|
resultsContainer: null,
|
|||
|
json: [],
|
|||
|
success: Function.prototype,
|
|||
|
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
|
|||
|
templateMiddleware: Function.prototype,
|
|||
|
sortMiddleware: function () {
|
|||
|
return 0
|
|||
|
},
|
|||
|
noResultsText: 'No results found',
|
|||
|
limit: 10,
|
|||
|
fuzzy: false,
|
|||
|
debounceTime: null,
|
|||
|
exclude: []
|
|||
|
}
|
|||
|
|
|||
|
let debounceTimerHandle
|
|||
|
const debounce = function (func, delayMillis) {
|
|||
|
if (delayMillis) {
|
|||
|
clearTimeout(debounceTimerHandle)
|
|||
|
debounceTimerHandle = setTimeout(func, delayMillis)
|
|||
|
} else {
|
|||
|
func.call()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
|
|||
|
|
|||
|
/* removed: const _$Templater_7 = require('./Templater') */;
|
|||
|
/* removed: const _$Repository_4 = require('./Repository') */;
|
|||
|
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
|
|||
|
const optionsValidator = _$OptionsValidator_3({
|
|||
|
required: requiredOptions
|
|||
|
})
|
|||
|
/* removed: const _$utils_9 = require('./utils') */;
|
|||
|
|
|||
|
window.SimpleJekyllSearch = function (_options) {
|
|||
|
const errors = optionsValidator.validate(_options)
|
|||
|
if (errors.length > 0) {
|
|||
|
throwError('You must specify the following required options: ' + requiredOptions)
|
|||
|
}
|
|||
|
|
|||
|
options = _$utils_9.merge(options, _options)
|
|||
|
|
|||
|
_$Templater_7.setOptions({
|
|||
|
template: options.searchResultTemplate,
|
|||
|
middleware: options.templateMiddleware
|
|||
|
})
|
|||
|
|
|||
|
_$Repository_4.setOptions({
|
|||
|
fuzzy: options.fuzzy,
|
|||
|
limit: options.limit,
|
|||
|
sort: options.sortMiddleware,
|
|||
|
exclude: options.exclude
|
|||
|
})
|
|||
|
|
|||
|
if (_$utils_9.isJSON(options.json)) {
|
|||
|
initWithJSON(options.json)
|
|||
|
} else {
|
|||
|
initWithURL(options.json)
|
|||
|
}
|
|||
|
|
|||
|
const rv = {
|
|||
|
search: search
|
|||
|
}
|
|||
|
|
|||
|
typeof options.success === 'function' && options.success.call(rv)
|
|||
|
return rv
|
|||
|
}
|
|||
|
|
|||
|
function initWithJSON (json) {
|
|||
|
_$Repository_4.put(json)
|
|||
|
registerInput()
|
|||
|
}
|
|||
|
|
|||
|
function initWithURL (url) {
|
|||
|
_$JSONLoader_2.load(url, function (err, json) {
|
|||
|
if (err) {
|
|||
|
throwError('failed to get JSON (' + url + ')')
|
|||
|
}
|
|||
|
initWithJSON(json)
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function emptyResultsContainer () {
|
|||
|
options.resultsContainer.innerHTML = ''
|
|||
|
}
|
|||
|
|
|||
|
function appendToResultsContainer (text) {
|
|||
|
options.resultsContainer.innerHTML += text
|
|||
|
}
|
|||
|
|
|||
|
function registerInput () {
|
|||
|
options.searchInput.addEventListener('input', function (e) {
|
|||
|
if (isWhitelistedKey(e.which)) {
|
|||
|
emptyResultsContainer()
|
|||
|
debounce(function () { search(e.target.value) }, options.debounceTime)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function search (query) {
|
|||
|
if (isValidQuery(query)) {
|
|||
|
emptyResultsContainer()
|
|||
|
render(_$Repository_4.search(query), query)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function render (results, query) {
|
|||
|
const len = results.length
|
|||
|
if (len === 0) {
|
|||
|
return appendToResultsContainer(options.noResultsText)
|
|||
|
}
|
|||
|
for (let i = 0; i < len; i++) {
|
|||
|
results[i].query = query
|
|||
|
appendToResultsContainer(_$Templater_7.compile(results[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isValidQuery (query) {
|
|||
|
return query && query.length > 0
|
|||
|
}
|
|||
|
|
|||
|
function isWhitelistedKey (key) {
|
|||
|
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
|
|||
|
}
|
|||
|
|
|||
|
function throwError (message) {
|
|||
|
throw new Error('SimpleJekyllSearch --- ' + message)
|
|||
|
}
|
|||
|
})(window)
|
|||
|
|
|||
|
}());
|
|||
|
</script>
|
|||
|
|
|||
|
<!-- Configuration -->
|
|||
|
<script>
|
|||
|
SimpleJekyllSearch({
|
|||
|
searchInput: document.getElementById('search-input'),
|
|||
|
resultsContainer: document.getElementById('results-container'),
|
|||
|
noResultsText: '<p>Aucun résultat!</p>',
|
|||
|
json: '/search.json',
|
|||
|
searchResultTemplate: '<li><a href="{url}">{date} {title}</a> (Création {create})</li>'
|
|||
|
})
|
|||
|
</script>
|
|||
|
|
|||
|
</div></div>
|
|||
|
|
|||
|
|
|||
|
<script>(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
function scrollToAnchor(anchor, duration, callback) {
|
|||
|
var $root = this;
|
|||
|
$root.animate({ scrollTop: $(anchor).position().top }, duration, function() {
|
|||
|
window.history.replaceState(null, '', window.location.href.split('#')[0] + anchor);
|
|||
|
callback && callback();
|
|||
|
});
|
|||
|
}
|
|||
|
$.fn.scrollToAnchor = scrollToAnchor;
|
|||
|
});
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
function affix(options) {
|
|||
|
var $root = this, $window = $(window), $scrollTarget, $scroll,
|
|||
|
offsetBottom = 0, scrollTarget = window, scroll = window.document, disabled = false, isOverallScroller = true,
|
|||
|
rootTop, rootLeft, rootHeight, scrollBottom, rootBottomTop,
|
|||
|
hasInit = false, curState;
|
|||
|
|
|||
|
function setOptions(options) {
|
|||
|
var _options = options || {};
|
|||
|
_options.offsetBottom && (offsetBottom = _options.offsetBottom);
|
|||
|
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
|
|||
|
_options.scroll && (scroll = _options.scroll);
|
|||
|
_options.disabled !== undefined && (disabled = _options.disabled);
|
|||
|
$scrollTarget = $(scrollTarget);
|
|||
|
isOverallScroller = window.isOverallScroller($scrollTarget[0]);
|
|||
|
$scroll = $(scroll);
|
|||
|
}
|
|||
|
function preCalc() {
|
|||
|
top();
|
|||
|
rootHeight = $root.outerHeight();
|
|||
|
rootTop = $root.offset().top + (isOverallScroller ? 0 : $scrollTarget.scrollTop());
|
|||
|
rootLeft = $root.offset().left;
|
|||
|
}
|
|||
|
function calc(needPreCalc) {
|
|||
|
needPreCalc && preCalc();
|
|||
|
scrollBottom = $scroll.outerHeight() - offsetBottom - rootHeight;
|
|||
|
rootBottomTop = scrollBottom - rootTop;
|
|||
|
}
|
|||
|
function top() {
|
|||
|
if (curState !== 'top') {
|
|||
|
$root.removeClass('fixed').css({
|
|||
|
left: 0,
|
|||
|
top: 0
|
|||
|
});
|
|||
|
curState = 'top';
|
|||
|
}
|
|||
|
}
|
|||
|
function fixed() {
|
|||
|
if (curState !== 'fixed') {
|
|||
|
$root.addClass('fixed').css({
|
|||
|
left: rootLeft + 'px',
|
|||
|
top: 0
|
|||
|
});
|
|||
|
curState = 'fixed';
|
|||
|
}
|
|||
|
}
|
|||
|
function bottom() {
|
|||
|
if (curState !== 'bottom') {
|
|||
|
$root.removeClass('fixed').css({
|
|||
|
left: 0,
|
|||
|
top: rootBottomTop + 'px'
|
|||
|
});
|
|||
|
curState = 'bottom';
|
|||
|
}
|
|||
|
}
|
|||
|
function setState() {
|
|||
|
var scrollTop = $scrollTarget.scrollTop();
|
|||
|
if (scrollTop >= rootTop && scrollTop <= scrollBottom) {
|
|||
|
fixed();
|
|||
|
} else if (scrollTop < rootTop) {
|
|||
|
top();
|
|||
|
} else {
|
|||
|
bottom();
|
|||
|
}
|
|||
|
}
|
|||
|
function init() {
|
|||
|
if(!hasInit) {
|
|||
|
var interval, timeout;
|
|||
|
calc(true); setState();
|
|||
|
// run calc every 100 millisecond
|
|||
|
interval = setInterval(function() {
|
|||
|
calc();
|
|||
|
}, 100);
|
|||
|
timeout = setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
}, 45000);
|
|||
|
window.pageLoad.then(function() {
|
|||
|
setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
clearTimeout(timeout);
|
|||
|
}, 3000);
|
|||
|
});
|
|||
|
$scrollTarget.on('scroll', function() {
|
|||
|
disabled || setState();
|
|||
|
});
|
|||
|
$window.on('resize', function() {
|
|||
|
disabled || (calc(true), setState());
|
|||
|
});
|
|||
|
hasInit = true;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
setOptions(options);
|
|||
|
if (!disabled) {
|
|||
|
init();
|
|||
|
}
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
init();
|
|||
|
}, 200));
|
|||
|
return {
|
|||
|
setOptions: setOptions,
|
|||
|
refresh: function() {
|
|||
|
calc(true, { animation: false }); setState();
|
|||
|
}
|
|||
|
};
|
|||
|
}
|
|||
|
$.fn.affix = affix;
|
|||
|
});
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
function toc(options) {
|
|||
|
var $root = this, $window = $(window), $scrollTarget, $scroller, $tocUl = $('<ul class="toc toc--ellipsis"></ul>'), $tocLi, $headings, $activeLast, $activeCur,
|
|||
|
selectors = 'h1,h2,h3', container = 'body', scrollTarget = window, scroller = 'html, body', disabled = false,
|
|||
|
headingsPos, scrolling = false, hasRendered = false, hasInit = false;
|
|||
|
|
|||
|
function setOptions(options) {
|
|||
|
var _options = options || {};
|
|||
|
_options.selectors && (selectors = _options.selectors);
|
|||
|
_options.container && (container = _options.container);
|
|||
|
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
|
|||
|
_options.scroller && (scroller = _options.scroller);
|
|||
|
_options.disabled !== undefined && (disabled = _options.disabled);
|
|||
|
$headings = $(container).find(selectors).filter('[id]');
|
|||
|
$scrollTarget = $(scrollTarget);
|
|||
|
$scroller = $(scroller);
|
|||
|
}
|
|||
|
function calc() {
|
|||
|
headingsPos = [];
|
|||
|
$headings.each(function() {
|
|||
|
headingsPos.push(Math.floor($(this).position().top));
|
|||
|
});
|
|||
|
}
|
|||
|
function setState(element, disabled) {
|
|||
|
var scrollTop = $scrollTarget.scrollTop(), i;
|
|||
|
if (disabled || !headingsPos || headingsPos.length < 1) { return; }
|
|||
|
if (element) {
|
|||
|
$activeCur = element;
|
|||
|
} else {
|
|||
|
for (i = 0; i < headingsPos.length; i++) {
|
|||
|
if (scrollTop >= headingsPos[i]) {
|
|||
|
$activeCur = $tocLi.eq(i);
|
|||
|
} else {
|
|||
|
$activeCur || ($activeCur = $tocLi.eq(i));
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
$activeLast && $activeLast.removeClass('active');
|
|||
|
($activeLast = $activeCur).addClass('active');
|
|||
|
}
|
|||
|
function render() {
|
|||
|
if(!hasRendered) {
|
|||
|
$root.append($tocUl);
|
|||
|
$headings.each(function() {
|
|||
|
var $this = $(this);
|
|||
|
$tocUl.append($('<li></li>').addClass('toc-' + $this.prop('tagName').toLowerCase())
|
|||
|
.append($('<a></a>').text($this.text()).attr('href', '#' + $this.prop('id'))));
|
|||
|
});
|
|||
|
$tocLi = $tocUl.children('li');
|
|||
|
$tocUl.on('click', 'a', function(e) {
|
|||
|
e.preventDefault();
|
|||
|
var $this = $(this);
|
|||
|
scrolling = true;
|
|||
|
setState($this.parent());
|
|||
|
$scroller.scrollToAnchor($this.attr('href'), 400, function() {
|
|||
|
scrolling = false;
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
hasRendered = true;
|
|||
|
}
|
|||
|
function init() {
|
|||
|
var interval, timeout;
|
|||
|
if(!hasInit) {
|
|||
|
render(); calc(); setState(null, scrolling);
|
|||
|
// run calc every 100 millisecond
|
|||
|
interval = setInterval(function() {
|
|||
|
calc();
|
|||
|
}, 100);
|
|||
|
timeout = setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
}, 45000);
|
|||
|
window.pageLoad.then(function() {
|
|||
|
setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
clearTimeout(timeout);
|
|||
|
}, 3000);
|
|||
|
});
|
|||
|
$scrollTarget.on('scroll', function() {
|
|||
|
disabled || setState(null, scrolling);
|
|||
|
});
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
if (!disabled) {
|
|||
|
render(); calc(); setState(null, scrolling);
|
|||
|
}
|
|||
|
}, 100));
|
|||
|
}
|
|||
|
hasInit = true;
|
|||
|
}
|
|||
|
|
|||
|
setOptions(options);
|
|||
|
if (!disabled) {
|
|||
|
init();
|
|||
|
}
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
init();
|
|||
|
}, 200));
|
|||
|
return {
|
|||
|
setOptions: setOptions
|
|||
|
};
|
|||
|
}
|
|||
|
$.fn.toc = toc;
|
|||
|
});
|
|||
|
})();
|
|||
|
/*(function () {
|
|||
|
|
|||
|
})();*/
|
|||
|
</script><script>
|
|||
|
/* toc must before affix, since affix need to konw toc' height. */(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
var TOC_SELECTOR = window.TEXT_VARIABLES.site.toc.selectors;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
var $window = $(window);
|
|||
|
var $articleContent = $('.js-article-content');
|
|||
|
var $tocRoot = $('.js-toc-root'), $col2 = $('.js-col-aside');
|
|||
|
var toc;
|
|||
|
var tocDisabled = false;
|
|||
|
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
|
|||
|
var hasToc = $articleContent.find(TOC_SELECTOR).length > 0;
|
|||
|
|
|||
|
function disabled() {
|
|||
|
return $col2.css('display') === 'none' || !hasToc;
|
|||
|
}
|
|||
|
|
|||
|
tocDisabled = disabled();
|
|||
|
|
|||
|
toc = $tocRoot.toc({
|
|||
|
selectors: TOC_SELECTOR,
|
|||
|
container: $articleContent,
|
|||
|
scrollTarget: hasSidebar ? '.js-page-main' : null,
|
|||
|
scroller: hasSidebar ? '.js-page-main' : null,
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
tocDisabled = disabled();
|
|||
|
toc && toc.setOptions({
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
}, 100));
|
|||
|
|
|||
|
});
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
var $window = $(window), $pageFooter = $('.js-page-footer');
|
|||
|
var $pageAside = $('.js-page-aside');
|
|||
|
var affix;
|
|||
|
var tocDisabled = false;
|
|||
|
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
|
|||
|
|
|||
|
affix = $pageAside.affix({
|
|||
|
offsetBottom: $pageFooter.outerHeight(),
|
|||
|
scrollTarget: hasSidebar ? '.js-page-main' : null,
|
|||
|
scroller: hasSidebar ? '.js-page-main' : null,
|
|||
|
scroll: hasSidebar ? $('.js-page-main').children() : null,
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
affix && affix.setOptions({
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
}, 100));
|
|||
|
|
|||
|
window.pageAsideAffix = affix;
|
|||
|
});
|
|||
|
})();
|
|||
|
</script><!---->
|
|||
|
|
|||
|
</div>
|
|||
|
<script>(function () {
|
|||
|
var $root = document.getElementsByClassName('root')[0];
|
|||
|
if (window.hasEvent('touchstart')) {
|
|||
|
$root.dataset.isTouch = true;
|
|||
|
document.addEventListener('touchstart', function(){}, false);
|
|||
|
}
|
|||
|
})();
|
|||
|
</script>
|
|||
|
</body>
|
|||
|
</html>
|
|||
|
|