Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

249
gaia/apps/music/js/view.js Executable file
View file

@ -0,0 +1,249 @@
/* exported View */
/* global SERVICE_WORKERS, bridge */
window.View = (function() {
'use strict';
var debug = 0 ? (...args) => console.log('[View]', ...args) : () => {};
if (!SERVICE_WORKERS) (function() {
window.ROUTES = {
'/api/activities/share/:filePath': 'share',
'/api/albums/list': 'getAlbums',
'/api/albums/info/:filePath': 'getAlbum',
'/api/artists/list': 'getArtists',
'/api/artists/info/:filePath': 'getArtist',
'/api/artwork/original/:filePath': 'getSongArtwork',
'/api/artwork/thumbnail/:filePath': 'getSongThumbnail',
'/api/artwork/url/original/:filePath': 'getSongArtworkURL',
'/api/artwork/url/thumbnail/:filePath': 'getSongThumbnailURL',
'/api/audio/play': 'play',
'/api/audio/pause': 'pause',
'/api/audio/seek/:time': 'seek',
'/api/audio/fastseek/start/:direction': 'startFastSeek',
'/api/audio/fastseek/stop': 'stopFastSeek',
'/api/audio/status': 'getPlaybackStatus',
'/api/database/status': 'getDatabaseStatus',
'/api/playlists/list': 'getPlaylists',
'/api/playlists/info/:id': 'getPlaylist',
'/api/queue/current': 'currentSong',
'/api/queue/previous': 'previousSong',
'/api/queue/next': 'nextSong',
'/api/queue/album/:filePath': 'queueAlbum',
'/api/queue/artist/:filePath': 'queueArtist',
'/api/queue/playlist/:id/shuffle': 'queuePlaylist',
'/api/queue/playlist/:id/song/:filePath': 'queuePlaylist',
'/api/queue/song/:filePath': 'queueSong',
'/api/queue/repeat/:repeat': 'setRepeatSetting',
'/api/queue/shuffle/:shuffle': 'setShuffleSetting',
'/api/songs/list': 'getSongs',
'/api/songs/count': 'getSongCount',
'/api/songs/info/:filePath': 'getSong',
'/api/songs/rating/:rating/:filePath': 'setSongRating',
'/api/search/:key/': 'search',
'/api/search/:key/:query': 'search'
};
for (var path in window.ROUTES) {
var method = window.ROUTES[path];
window.ROUTES[path] = parseSimplePath(path);
window.ROUTES[path].method = method;
}
})();
function View() {
this.client = bridge.client({
service: 'music-service',
endpoint: window.parent,
timeout: false
});
this.params = {};
var parts = window.parent.location.href.split('?');
parts.shift();
var query = parts.join('?');
query.split('&').forEach((param) => {
var parts = param.split('=');
this.params[parts[0]] = parts[1];
});
window.addEventListener('click', (evt) => {
var link = evt.target.closest('a');
if (link) {
evt.preventDefault();
this.client.method('navigate', link.getAttribute('href'));
}
});
window.addEventListener('viewdestroy', () => this.destroy());
document.addEventListener('DOMRetranslated', () => {
this.title = document.title;
});
}
View.prototype.setupSearch = function(params) {
this.searchBox.addEventListener('search', (evt) => this.search(evt.detail));
this.searchResults.addEventListener('open', () => {
this.client.method('searchOpen');
});
this.searchResults.addEventListener('close', () => {
this.client.method('searchClose');
this.list.scrollTop = this.searchBox.HEIGHT;
});
this.searchResults.addEventListener('resultclick', (evt) => {
var link = evt.detail;
if (link) {
this.client.method('navigate', link.getAttribute('href'));
}
});
this.searchResults.getItemImageSrc = (item) => this.getThumbnail(item.name);
};
View.prototype.setupList = function() {
this.list.minScrollHeight = `calc(100% + ${this.searchBox.HEIGHT}px)`;
this.list.offset = this.searchBox.HEIGHT;
this.list.configure({
getItemImageSrc: (item) => this.getThumbnail(item.name)
});
// Show the view only when list has something
// rendered, this prevents Gecko painting unnecessarily.
this.list.rendered.then(() => document.body.hidden = false);
this.list.scrollTo(this.searchBox.HEIGHT);
};
View.prototype.destroy = function() {
Object.getOwnPropertyNames(this).forEach(prop => this[prop] = null);
debug('Destroyed');
};
View.prototype.render = function() {
// No-op in the base class.
};
View.prototype.onRenderDone = function() {
if (window.frameElement) {
window.frameElement.dispatchEvent(new CustomEvent('rendered'));
}
debug('Rendered');
};
View.prototype.fetch = SERVICE_WORKERS ?
function(url) {
return window.fetch(encodeURI(url));
} :
function(url) {
for (var path in window.ROUTES) {
var route = window.ROUTES[path];
var match = url.match(route.regexp);
if (match) {
return new Promise((resolve) => {
setTimeout(() => {
var args = [route.method].concat(match.splice(1));
this.client.method.apply(this.client, args).then((result) => {
resolve({
blob: () => Promise.resolve(result),
json: () => Promise.resolve(result)
});
});
});
});
}
}
return Promise.reject();
};
Object.defineProperty(View.prototype, 'title', {
get: function() {
return document.title;
},
set: function(value) {
document.title = value;
window.frameElement.dispatchEvent(new CustomEvent('titlechange', {
detail: document.title
}));
}
});
View.extend = function(subclass) {
subclass.prototype = Object.create(View.prototype, {
constructor: {
value: subclass,
enumerable: false,
writable: true,
configurable: true
}
});
// subclass.__proto__ = View;
return subclass;
};
/**
* Route parser from components/serviceworkerware/dist/sww.js
*/
function parseSimplePath(path) {
// Check for named placeholder crowding
if (/\:[a-zA-Z0-9]+\:[a-zA-Z0-9]+/g.test(path)) {
throw new Error('Invalid usage of named placeholders');
}
// Check for mixed placeholder crowdings
var mixedPlaceHolders =
/(\*\:[a-zA-Z0-9]+)|(\:[a-zA-Z0-9]+\:[a-zA-Z0-9]+)|(\:[a-zA-Z0-9]+\*)/g;
if (mixedPlaceHolders.test(path.replace(/\\\*/g,''))) {
throw new Error('Invalid usage of named placeholders');
}
// Try parsing the string and converting special characters into regex
try {
// Parsing anonymous placeholders with simple backslash-escapes
path = path.replace(/(.|^)[*]+/g, function(m,escape) {
return escape==='\\' ? '\\*' : (escape+'(?:.*?)');
});
// Parsing named placeholders with backslash-escapes
var tags = [];
path = path.replace(/(.|^)\:([a-zA-Z0-9]+)/g, function (m, escape, tag) {
if (escape === '\\') { return ':' + tag; }
tags.push(tag);
return escape + '(.+?)';
});
return { regexp: RegExp(path + '$'), tags: tags };
}
// Failed to parse final path as a RegExp
catch (ex) {
throw new Error('Invalid path specified');
}
}
return View;
})();