Commit f806d05a authored by Luxferre's avatar Luxferre
Browse files

Initial commit

parents
# FastLog: a better call log app for KaiOS
## Why?
Because during these years, lots of people have noticed that stock KaiOS call log is too slow and unreliable on large amount of records (over 100 etc.) and is bloated with features that not everyone needs for the basic phone usage.
## What can it do right now?
- Log all outgoing/incoming calls and add them to the search index;
- Search the records in T9-like fashion (can be done by number or any part of the contact name);
- Import and index the call log records existing before FastLog installation;
- Show the type (outgoing/incoming/missed), number, contact name (if any), SIM, time (respects the system 12/24-hour setting), date and duration of the call in each record;
- Allow to delete records, add them to new or existing contacts (from the menu);
- Allow to call or text the number from the record (from the menu or via Call and SoftLeft keys respectively);
- Allow to block/unblock the calls from the number in a particular record (from the menu);
- Understand some Bluetooth HFP commands from wireless headsets, like "last number redial" or "Dial a number directly" or "Dial a number from the outgoing memory";
- ...and even activate/deactivate the flashlight by long-pressing the Up arrow!
## How to install it?
Via WebIDE or `gdeploy`, as usual.
## What's the project status?
Public-domain alpha. Until the version 1.0, it's a fully rolling release, so please don't pay attention to the version stated in the manifest - it's for internal tracking convenience only.
## Who's behind it?
Someone who cares.
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
* {box-sizing: border-box}
html, body {
margin: 0;
padding: 0;
width: 100%;
height:100%;
position: fixed;
left: 0;
top: 0;
overflow-y: scroll;
overflow-x: hidden;
}
body {
font-size: 1rem;
background: #000;
color: #eee;
font-family: "Open Sans", OpenSans, sans-serif;
}
main {
margin: 0;
padding: 26px 0 0;
background: #111;
}
.clist {
display: block;
margin: 0;
padding: 0;
overflow-x: hidden;
overflow-y: hidden;
width: 100%;
height: calc(100vh - 56px);
}
.clist.withsearch {
height: calc(100vh - 56px - 50px);
}
.clist>[data-list-id] {
display: block;
width: 100%;
padding: 0 0;
height: 53px;
position: relative;
}
.clist>[data-list-id].active {
background: #017ee5;
}
.clist>[data-list-id]>span {
display: inline-flex;
vertical-align: middle;
width: 100%;
height: 53px;
flex-flow: column wrap;
justify-content: space-around;
}
/* app menu styling begin */
.appmenu {
position: fixed;
bottom: 30px;
display: block;
margin: 0;
padding: 0;
overflow-x: hidden;
overflow-y: scroll;
width: 100%;
height: auto;
z-index: 888;
}
.appmenu>[data-list-id] {
display: block;
width: 100%;
padding: 0.5rem 1rem;
height: 20%;
background: #111;
}
.appmenu>[data-list-id].active {
background: #017ee5!important;
}
.appmenu[data-menu-key="mainmenu"] {
text-align: right;
}
.appmenu[data-menu-key="mainmenu"]>[data-list-id] {
width: 86%;
text-align: left;
display: inline-block;
background: #131313;
}
/* app menu styling end */
.hidden {display:none!important}
#softkeys {
position: fixed; bottom: 0; left: 0; width: 100%; height: 30px;
background: #111;
padding: 0;
color: #ddd;
}
#softkeys > * {
display: inline-block;
width: 72px;
padding: 4px 5px;
}
#softkey-left, #softkey-right {
font-weight: 400;
font-size: 14px;
}
#softkey-right {
text-align: right;
}
#softkey-center {
font-weight: 600;
font-size: 16px;
width: 88px;
text-align: center;
}
.toast {
position:fixed;
left:0;
top: -54px;
width: 100%;
background-color: #363636;
min-height: 36px;
color: #ffffff;
text-align: center;
word-wrap: break-word;
opacity: 0;
z-index: 99999;
transition: transform 0.25s, opacity 0.25s, top 0.25s, bottom 0.25s;
box-sizing: border-box;
max-width: 18rem;
padding: 26px 8px 8px;
}
.toast.active {
opacity: 1;
top: 0;
}
.translucent {
opacity: 0.4!important;
}
.contact-device, .contact-sim1, .contact-sim2 {
max-width: calc(100% - 30px);
}
.contact-device:after, .contact-sim1:after, .contact-sim2:after {
content: '';
display: inline-block;
position: absolute;
top: 9px;
right: 10px;
height: 35px;
}
.contact-device:after {
background: url(img/sim-icons.png) -53px 0;
right: 16px;
width: 15px;
}
.contact-sim1:after {
background: url(img/sim-icons.png) 0 0;
width: 26px;
}
.contact-sim2:after {
background: url(img/sim-icons.png) -27px 0;
width: 26px;
}
.searchdial {
display: block;
position: fixed;
left: 0;
bottom: 30px;
height: 50px;
padding: 3px 1rem;
text-align: center;
font-size: 2rem;
width:100%;
background: #02345d;
}
.loadscreen {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.88);
color: #eee;
font-size: 2rem;
text-align: center;
}
.loadscreen > span {
display: inline-block;
}
[data-icon]::before {
font-size:3rem!important;
display: inline-block;
vertical-align: middle;
}
.contact-name {
height:21px;
width: 68vw;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.contact-number {
font-size: 11px;
color: #aaa;
width: 68vw;
height: 14px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.call-meta {
font-size: 11px;
color: #999;
}
.active .contact-number, .active .call-meta {
color: #eee;
}
.blocked span.contact-number, .blocked span.contact-name {
text-decoration: line-through;
color: #777!important;
}
@font-face {
font-family: "gaia-icons";
src: url("fonts/gaia-icons.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-variant-numeric: nominal;
}
[data-icon]:before,
.ligature-icons {
font-family: "gaia-icons";
content: attr(data-icon);
display: inline-block;
font-weight: 500;
font-style: normal;
text-decoration: inherit;
text-transform: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
<!DOCTYPE html>
<head>
<title>FastLog</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="stylesheet" type="text/css" href="gaia-icons/gaia-icons.css">
<link rel="stylesheet" type="text/css" href="app.css">
</head>
<body>
<main>
<section class="clist"></section>
<section class="appmenu hidden"></section>
</main>
<footer id="softkeys">
<div id="softkey-left"></div>
<div id="softkey-center"></div>
<div id="softkey-right"></div>
</footer>
<div class="toast"></div>
<div class="searchdial hidden"></div>
<div class="loadscreen"><span>Indexing, please wait&hellip;</span></div>
<script src="js/shared/alphaindex.js"></script>
<script src="js/shared/dynalist.js"></script>
<script src="js/shared/appmenu.js"></script>
<script src="js/shared/mozcaller.js"></script>
<script src="js/shared/longpress.js"></script>
<script src="js/calllogmgr.js"></script>
<script src="js/app.js"></script>
</body>
(function(global, nav) {
var currentPage,
//DOM elements
editor, logRecordView, dialerField, loadScreen,
softLeft, softCenter, softRight, toastContainer,
//internal variables
directDial, backendRecords, searchBuffer, indexInProgress,
//DynaLists
mainList,
//misc stuff
h12Setting = false, SKS = [], fullIdCache = {}, flashlightMgr = null
//backend functions
function escapeName(str) {
return str.replace(/>/g,'&gt;').replace(/</g,'&lt;')
}
function searchBufferUndo() {
searchBuffer = searchBuffer.slice(0,-1)
}
function detectSims() {
var i, detectedSims = [], conns = nav.mozMobileConnections, l = conns.length;
for(i=0;i<l;i++) {
if(conns[i].voice.connected)
detectedSims.push({connId: i, network: conns[i].voice.network.shortName})
}
return detectedSims
}
function selectSIM(cb) {
var sims = detectSims(), l = sims.length
if(l) {
if(l > 1) {
var i, opts = []
for(i=0;i<sims.length;i++) (function(i, net){
opts.push(['SIM ' + (i+1) + ' (' + net + ')', function(){cb(i)}])
})(i, sims[i].network)
AppMenu.register('simselect', opts, function(){
toggleVisualMode('numberSelector')
stashSoftKeys()
updateSoftKeys('Cancel', 'USE', '')
}, function(){
restoreSoftKeys()
logRecordView.classList.remove('translucent')
cb(-1)
}
)
AppMenu.open('simselect')
}
else cb(0)
}
else toast('No valid SIMs')
}
function doCall(selectedNumber) {
selectSIM(function(simId) {
if(simId > -1 && MozCaller.isSimActive(simId))
MozCaller.dial(selectedNumber, simId)
else toast('Call failed, connection unavailable')
})
}
function callRecord(recId) {
var number = backendRecords[recId|0].number
doCall(number)
}
//frontend functions
function toast(msg) {
toastContainer.textContent = msg
toastContainer.classList.add('active')
setTimeout(function() {
toastContainer.classList.remove('active')
}, 4000)
}
function updateSoftKeys(leftText, centerText, rightText) {
softLeft.textContent = leftText
softCenter.textContent = centerText
softRight.textContent = rightText
}
function stashSoftKeys() {
SKS = [
softLeft.textContent,
softCenter.textContent,
softRight.textContent
]
}
function restoreSoftKeys() {
softLeft.textContent = SKS[0]
softCenter.textContent = SKS[1]
softRight.textContent = SKS[2]
}
function toggleVisualMode(component) {
logRecordView.classList.add('hidden')
switch(component) {
case 'main':
logRecordView.classList.remove('hidden')
logRecordView.classList.remove('translucent')
updateSearchView()
break
case 'numberSelector':
dialerField.classList.add('hidden')
logRecordView.classList.remove('withsearch')
logRecordView.classList.remove('hidden')
logRecordView.classList.add('translucent')
break
case 'appMenuMain':
logRecordView.classList.remove('hidden')
logRecordView.classList.add('translucent')
break
default:
break
}
}
function searchAndRender(searchStr) {
if(searchStr === '') {
mainList.filter(null)
return null
} else {
var results = CallLogMgr.find(searchStr), validListIds = [], i, l = results.length
for(i=0;i<l;i++)
validListIds.push(fullIdCache[results[i].id])
mainList.filter(validListIds)
return validListIds
}
}
function getTimeStr(timestamp) {
return (new Date(timestamp)).toLocaleTimeString(nav.language, {hour12: h12Setting, timeStyle: 'short', hour: '2-digit', minute: '2-digit'})
}
function getDateStr(timestamp) {
return (new Date(timestamp)).toLocaleDateString(nav.language, {dateStyle: 'short', year: '2-digit', month: '2-digit', day: '2-digit'})
}
function align2(s) {
return ('00' + s).slice(-2)
}
function getDurationStr(durationMs) {
var totalSeconds = Math.ceil(durationMs/1000), h, m, s
h = 0|(totalSeconds / 3600)
totalSeconds = totalSeconds - h*3600
m = 0|(totalSeconds / 60)
s = totalSeconds%60
var m_s = align2(m) + ':' + align2(s)
return h ? (h + ':' + m_s) : m_s
}
function initialRender() {
var results = CallLogMgr.find(''), names = [], i, l = results.length
var ctypes = ['sim1', 'sim2'], foundIds = []
backendRecords = []
fullIdCache = {}
for(i=0;i<l;i++) {
foundIds.push(results[i].id)
backendRecords.push(results[i])
fullIdCache[results[i].id] = i
var contactNumberField = '', callMeta = getTimeStr(results[i].timestamp) + '&nbsp;' + getDateStr(results[i].timestamp)
if(results[i].duration)
callMeta += '&nbsp;' + getDurationStr(results[i].duration)
if(results[i].contactName)
contactNumberField = '<span class="contact-number">' + escapeName(results[i].number) + '</span>'
names.push('<span class="contact-' + ctypes[results[i].simId] + (results[i].blocked ? ' blocked' : '') + '" data-icon="call-' + results[i].type + '">'
+ '<span class="contact-name">' + escapeName(results[i].contactName || results[i].number) + '</span>'
+ contactNumberField + '<span class="call-meta">' + callMeta + '</span>'
+ '</span>')
}
mainList.render(names, true)
mainList.update()
return foundIds
}
function openMain(forceReindex, blocking) {
currentPage = 'main'
updateSoftKeys('SMS', 'CALL', 'Options')
if(blocking) {
loadScreen.classList.remove('hidden')
indexInProgress = true
}
CallLogMgr.init(forceReindex, function() { //on load
if(blocking) {
indexInProgress = false
loadScreen.classList.add('hidden')
}
if(forceReindex)
toast('Call log reindexed')
initialRender()
if(blocking) searchBuffer = ''
updateSearchView()
}, function() { //on list update
initialRender()
updateSearchView()
})
}
function updateSearchView() {
var validRecordIds = searchAndRender(searchBuffer)
dialerField.textContent = searchBuffer.slice(-12)
directDial = false
if(searchBuffer.length) {
dialerField.classList.remove('hidden')
if(!logRecordView.classList.contains('withsearch'))
logRecordView.classList.add('withsearch')
if(validRecordIds !== null && validRecordIds.length === 0)
directDial = true
}
else {
dialerField.classList.add('hidden')
logRecordView.classList.remove('withsearch')
}
}
function openAppMenu() {
var vmClass = 'appMenuMain', opts = [], curId = null
if(mainList.hasDisplayableItems()) {
opts.push(['Add to new contact', function(){
CallLogMgr.toNewContact(curId)
}])
opts.push(['Add to existing contact', function(){
CallLogMgr.toExistingContact(curId)
}])
opts.push(['Message', function(){
CallLogMgr.toSMS(curId)
}])
opts.push(['Block/unblock', function(){
var rec = backendRecords[curId], recName = rec.contactName || rec.number
if(rec.blocked) {
if(global.confirm('Unblock ' + recName + '?'))
CallLogMgr.unblock(curId, function() {
toast('Number unblocked')
openMain(false, false)
})
}
else {
if(global.confirm('Block ' + recName + '?'))
CallLogMgr.block(curId, function() {
toast('Number blocked')
openMain(false, false)
})
}
}])
opts.push(['Delete', function(){
if(global.confirm('Delete the call log entry? This action acnnot be undone!')) {
CallLogMgr.remove(curId)
toast('Entry deleted')
openMain(true, false)
}
}])