Merge branch 'upcoming' into fuck-git
1
.gitignore
vendored
|
@ -4,6 +4,7 @@ dist
|
|||
yarn*
|
||||
package-lock.json
|
||||
.yarnclean
|
||||
build
|
||||
|
||||
# Misc
|
||||
.idea
|
||||
|
|
262
index.js
|
@ -1,262 +0,0 @@
|
|||
require('v8-compile-cache');
|
||||
const { app, components } = require('electron'), { resolve, join } = require("path"),
|
||||
CiderBase = require('./src/main/cider-base');
|
||||
const customProtocols = require('./package.json').fileAssociations[0].protocols
|
||||
console.log(customProtocols)
|
||||
const comps = components;
|
||||
|
||||
|
||||
// Analytics for debugging.
|
||||
const ElectronSentry = require("@sentry/electron");
|
||||
ElectronSentry.init({ dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214" });
|
||||
|
||||
const configDefaults = {
|
||||
"general": {
|
||||
"close_behavior": 0, // 0 = close, 1 = minimize, 2 = minimize to tray
|
||||
"startup_behavior": 0, // 0 = nothing, 1 = open on startup
|
||||
"discord_rpc": 1, // 0 = disabled, 1 = enabled as Cider, 2 = enabled as Apple Music
|
||||
"discordClearActivityOnPause": 1, // 0 = disabled, 1 = enabled
|
||||
"volume": 1
|
||||
},
|
||||
"home": {
|
||||
"followedArtists": [],
|
||||
"favoriteItems": []
|
||||
},
|
||||
"libraryPrefs": {
|
||||
"songs": {
|
||||
"sort": "name",
|
||||
"sortOrder": "asc",
|
||||
"size": "normal"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"quality": "990",
|
||||
"seamless_audio": true,
|
||||
"normalization": false,
|
||||
"spatial": false,
|
||||
"spatial_properties": {
|
||||
"presets": [],
|
||||
"gain": 0.8,
|
||||
"listener_position": [0, 0, 0],
|
||||
"audio_position": [0, 0, 0],
|
||||
"room_dimensions": {
|
||||
"width": 32,
|
||||
"height": 12,
|
||||
"depth": 32
|
||||
},
|
||||
"room_materials": {
|
||||
"left": 'metal',
|
||||
"right": 'metal',
|
||||
"front": 'brick-bare',
|
||||
"back": 'brick-bare',
|
||||
"down": 'acoustic-ceiling-tiles',
|
||||
"up": 'acoustic-ceiling-tiles',
|
||||
}
|
||||
}
|
||||
},
|
||||
"visual": {
|
||||
"theme": "",
|
||||
"scrollbars": 0, // 0 = show on hover, 2 = always hide, 3 = always show
|
||||
"refresh_rate": 0,
|
||||
"animated_artwork": "limited", // 0 = always, 1 = limited, 2 = never
|
||||
"animated_artwork_qualityLevel": 1,
|
||||
"bg_artwork_rotation": false,
|
||||
"hw_acceleration": "default", // default, webgpu, disabled
|
||||
"videoRes": 720
|
||||
},
|
||||
"lyrics": {
|
||||
"enable_mxm": false,
|
||||
"mxm_karaoke": false,
|
||||
"mxm_language": "en",
|
||||
"enable_yt": false,
|
||||
},
|
||||
"lastfm": {
|
||||
"enabled": false,
|
||||
"scrobble_after": 50,
|
||||
"auth_token": "",
|
||||
"enabledRemoveFeaturingArtists": true,
|
||||
"NowPlaying": "true"
|
||||
},
|
||||
"advanced": {
|
||||
"AudioContext": false,
|
||||
"experiments": []
|
||||
}
|
||||
}
|
||||
|
||||
const merge = (target, source) => {
|
||||
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
|
||||
for (const key of Object.keys(source)) {
|
||||
if (source[key] instanceof Object) Object.assign(source[key], merge(target[key], source[key]))
|
||||
}
|
||||
// Join `target` and modified `source`
|
||||
Object.assign(target || {}, source)
|
||||
return target
|
||||
}
|
||||
|
||||
|
||||
const Store = require("electron-store");
|
||||
app.cfg = new Store({
|
||||
defaults: configDefaults
|
||||
});
|
||||
let currentCfg = app.cfg.get()
|
||||
app.cfg.set(merge(configDefaults, currentCfg))
|
||||
|
||||
app.paths = {
|
||||
ciderCache: resolve(app.getPath("userData"), "CiderCache"),
|
||||
themes: resolve(app.getPath("userData"), "Themes"),
|
||||
plugins: resolve(app.getPath("userData"), "Plugins"),
|
||||
}
|
||||
|
||||
switch (app.cfg.get("visual.hw_acceleration")) {
|
||||
default:
|
||||
case "default":
|
||||
app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode')
|
||||
app.commandLine.appendSwitch('enable-accelerated-video')
|
||||
app.commandLine.appendSwitch('disable-gpu-driver-bug-workarounds')
|
||||
app.commandLine.appendSwitch('ignore-gpu-blacklist')
|
||||
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers')
|
||||
app.commandLine.appendSwitch('enable-accelerated-video-decode');
|
||||
app.commandLine.appendSwitch('enable-gpu-rasterization');
|
||||
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
|
||||
app.commandLine.appendSwitch('enable-oop-rasterization');
|
||||
break;
|
||||
case "webgpu":
|
||||
console.info("WebGPU is enabled.");
|
||||
app.commandLine.appendSwitch('enable-unsafe-webgpu')
|
||||
break;
|
||||
case "disabled":
|
||||
console.info("Hardware acceleration is disabled.");
|
||||
app.commandLine.appendSwitch('disable-gpu')
|
||||
break;
|
||||
}
|
||||
|
||||
// Creating the Application Window and Calling all the Functions
|
||||
function CreateWindow() {
|
||||
if (app.isQuiting) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
/** CIDER **/
|
||||
const ciderwin = require("./src/main/cider-base")
|
||||
app.win = ciderwin
|
||||
app.win.Start()
|
||||
/** CIDER **/
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('no-sandbox');
|
||||
// app.commandLine.appendSwitch('js-flags', '--max-old-space-size=1024')
|
||||
|
||||
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* App Event Handlers
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
|
||||
|
||||
app.whenReady().then(async() => {
|
||||
if (process.platform === "win32") {
|
||||
app.commandLine.appendSwitch('high-dpi-support', 'true')
|
||||
app.commandLine.appendSwitch('force-device-scale-factor', '1')
|
||||
app.commandLine.appendSwitch('disable-pinch');
|
||||
}
|
||||
if (comps == null) {
|
||||
app.on("widevine-ready", () => {
|
||||
console.log('[Cider] Application is Ready. Creating Window.')
|
||||
if (!app.isPackaged) {
|
||||
console.info('[Cider] Running in development mode.')
|
||||
require('vue-devtools').install()
|
||||
}
|
||||
CreateWindow()
|
||||
})
|
||||
return
|
||||
}
|
||||
await comps.whenReady();
|
||||
console.log('components ready:', comps.status());
|
||||
|
||||
console.log('[Cider] Application is Ready. Creating Window.')
|
||||
if (!app.isPackaged) {
|
||||
console.info('[Cider] Running in development mode.')
|
||||
require('vue-devtools').install()
|
||||
}
|
||||
CreateWindow()
|
||||
})
|
||||
|
||||
|
||||
app.on('before-quit', () => {
|
||||
console.warn(`${app.getName()} exited.`);
|
||||
});
|
||||
|
||||
// Widevine Stuff
|
||||
app.on('widevine-ready', (version, lastVersion) => {
|
||||
if (null !== lastVersion) {
|
||||
console.log('[Cider][Widevine] Widevine ' + version + ', upgraded from ' + lastVersion + ', is ready to be used!')
|
||||
} else {
|
||||
console.log('[Cider][Widevine] Widevine ' + version + ' is ready to be used!')
|
||||
}
|
||||
})
|
||||
|
||||
app.on('widevine-update-pending', (currentVersion, pendingVersion) => {
|
||||
console.log('[Cider][Widevine] Widevine ' + currentVersion + ' is ready to be upgraded to ' + pendingVersion + '!')
|
||||
})
|
||||
|
||||
app.on('widevine-error', (error) => {
|
||||
console.log('[Cider][Widevine] Widevine installation encountered an error: ' + error)
|
||||
app.exit()
|
||||
})
|
||||
|
||||
if (process.defaultApp) {
|
||||
if (process.argv.length >= 2) {
|
||||
customProtocols.forEach((customProtocol) => {
|
||||
app.setAsDefaultProtocolClient(customProtocol, process.execPath, [resolve(process.argv[1])])
|
||||
})
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* cider - Custom Cider Protocol
|
||||
* ame - Custom AME Protocol (Backwards Compat.)
|
||||
* itms - iTunes HTTP Protocol
|
||||
* itmss - iTunes HTTPS Protocol
|
||||
* musics - macOS Client Protocol
|
||||
* music - macOS Client Protocol
|
||||
*/
|
||||
customProtocols.forEach((customProtocol) => {
|
||||
app.setAsDefaultProtocolClient(customProtocol)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
if (customProtocols.some(protocol => url.includes(protocol))) {
|
||||
CiderBase.LinkHandler(url)
|
||||
}
|
||||
})
|
||||
|
||||
app.on('second-instance', (_e, argv) => {
|
||||
console.warn(`[InstanceHandler][SecondInstanceHandler] Second Instance Started with args: [${argv.join(', ')}]`)
|
||||
|
||||
// Checks if first instance is authorized and if second instance has protocol args
|
||||
argv.forEach((value) => {
|
||||
if (customProtocols.some(protocol => value.includes(protocol))) {
|
||||
CiderBase.LinkHandler(value);
|
||||
}
|
||||
})
|
||||
|
||||
if (argv.includes("--force-quit")) {
|
||||
console.warn('[InstanceHandler][SecondInstanceHandler] Force Quit found. Quitting App.');
|
||||
// app.isQuiting = true
|
||||
app.quit()
|
||||
} else if (CiderBase.win && true) { // If a Second Instance has Been Started
|
||||
console.warn('[InstanceHandler][SecondInstanceHandler] Showing window.');
|
||||
app.win.show()
|
||||
app.win.focus()
|
||||
}
|
||||
})
|
||||
|
||||
if (!app.requestSingleInstanceLock() && true) {
|
||||
console.warn("[InstanceHandler] Existing Instance is Blocking Second Instance.");
|
||||
app.quit();
|
||||
// app.isQuiting = true
|
||||
}
|
33
package.json
|
@ -5,7 +5,8 @@
|
|||
"version": "1.0.0",
|
||||
"description": "A new look into listening and enjoying music in style and performance.",
|
||||
"license": "MIT",
|
||||
"author": "Cider Collective <cryptofyre@cryptofyre.org> (https://cider.sh)",
|
||||
"main": "./build/index.js",
|
||||
"author": "Cider Collective <cryptofyre@cider.sh> (https://cider.sh)",
|
||||
"repository": "https://github.com/ciderapp/Cider.git",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ciderapp/Cider/issues?q=is%3Aopen+is%3Aissue+label%3Abug"
|
||||
|
@ -13,10 +14,14 @@
|
|||
"homepage": "https://cider.sh/",
|
||||
"buildResources": "resources",
|
||||
"scripts": {
|
||||
"init": "yarn install --force",
|
||||
"start": "electron . --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers",
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"start": "run-script-os",
|
||||
"start:win32": "npm run build && set ELECTRON_ENABLE_LOGGING=true && electron ./build/index.js --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers",
|
||||
"start:linux": "npm run build && export ELECTRON_ENABLE_LOGGING=true && electron ./build/index.js --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers",
|
||||
"start:darwin": "npm run build && export ELECTRON_ENABLE_LOGGING=true && electron ./build/index.js --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder",
|
||||
"dist": "npm run build && electron-builder",
|
||||
"msft": "electron-builder -c msft-package.json",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
},
|
||||
|
@ -25,7 +30,7 @@
|
|||
"discord-rpc": "^4.0.1",
|
||||
"ejs": "^3.1.6",
|
||||
"electron-fetch": "^1.7.4",
|
||||
"electron-log": "^4.4.3",
|
||||
"electron-log": "^4.4.4",
|
||||
"electron-store": "^8.0.1",
|
||||
"electron-updater": "^4.6.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
|
@ -37,17 +42,20 @@
|
|||
"qrcode-terminal": "^0.12.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"run-script-os": "^1.1.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"v8-compile-cache": "^2.3.0",
|
||||
"ws": "^8.3.0",
|
||||
"ws": "^8.4.2",
|
||||
"xml2js": "^0.4.23",
|
||||
"youtube-search-without-api-key": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.13",
|
||||
"electron": "https://github.com/castlabs/electron-releases.git",
|
||||
"electron-builder": "^22.14.5",
|
||||
"electron-webpack": "^2.8.2",
|
||||
"musickit-typescript": "^1.2.4",
|
||||
"typescript": "^4.5.4",
|
||||
"vue-devtools": "^5.1.4",
|
||||
"webpack": "~5.65.0"
|
||||
},
|
||||
|
@ -74,9 +82,9 @@
|
|||
}
|
||||
],
|
||||
"build": {
|
||||
"electronVersion": "16.0.6",
|
||||
"electronVersion": "16.0.7",
|
||||
"electronDownload": {
|
||||
"version": "16.0.6+wvcus",
|
||||
"version": "16.0.7+wvcus",
|
||||
"mirror": "https://github.com/castlabs/electron-releases/releases/download/v"
|
||||
},
|
||||
"appId": "cider",
|
||||
|
@ -95,9 +103,9 @@
|
|||
],
|
||||
"extends": null,
|
||||
"files": [
|
||||
"**/*",
|
||||
"./src/**/*",
|
||||
"./resources/icons/icon.*"
|
||||
"./build/**/*",
|
||||
"./resources/icons/icon.*",
|
||||
"./src/**/*"
|
||||
],
|
||||
"linux": {
|
||||
"target": [
|
||||
|
@ -126,7 +134,8 @@
|
|||
"icon": "resources/icons/icon.ico"
|
||||
},
|
||||
"directories": {
|
||||
"buildResources": "."
|
||||
"buildResources": ".",
|
||||
"output": "dist"
|
||||
},
|
||||
"mac": {
|
||||
"icon": "./resources/icons/icon.icns",
|
||||
|
|
142
src/main/base/app.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import * as electron from 'electron';
|
||||
import * as path from 'path';
|
||||
|
||||
export class AppEvents {
|
||||
private static protocols: any = [
|
||||
"ame",
|
||||
"cider",
|
||||
"itms",
|
||||
"itmss",
|
||||
"musics",
|
||||
"music"
|
||||
]
|
||||
|
||||
private static store: any = null;
|
||||
|
||||
constructor(store: any) {
|
||||
console.log('App started');
|
||||
|
||||
AppEvents.store = store
|
||||
AppEvents.start(store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all actions that occur for the app on start (Mainly commandline arguments)
|
||||
* @returns {void}
|
||||
*/
|
||||
private static start(store: any): void {
|
||||
console.log('App started');
|
||||
|
||||
/**********************************************************************************************************************
|
||||
* Startup arguments handling
|
||||
**********************************************************************************************************************/
|
||||
if (electron.app.commandLine.hasSwitch('version') || electron.app.commandLine.hasSwitch('v')) {
|
||||
console.log(electron.app.getVersion())
|
||||
electron.app.exit()
|
||||
}
|
||||
|
||||
// Verbose Check
|
||||
if (electron.app.commandLine.hasSwitch('verbose')) {
|
||||
console.log("[Apple-Music-Electron] User has launched the application with --verbose");
|
||||
}
|
||||
|
||||
// Log File Location
|
||||
if (electron.app.commandLine.hasSwitch('log') || electron.app.commandLine.hasSwitch('l')) {
|
||||
console.log(path.join(electron.app.getPath('userData'), 'logs'))
|
||||
electron.app.exit()
|
||||
}
|
||||
|
||||
/***********************************************************************************************************************
|
||||
* Commandline arguments
|
||||
**********************************************************************************************************************/
|
||||
switch (store.get("visual.hw_acceleration")) {
|
||||
default:
|
||||
case "default":
|
||||
electron.app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode')
|
||||
electron.app.commandLine.appendSwitch('enable-accelerated-video')
|
||||
electron.app.commandLine.appendSwitch('disable-gpu-driver-bug-workarounds')
|
||||
electron.app.commandLine.appendSwitch('ignore-gpu-blacklist')
|
||||
electron.app.commandLine.appendSwitch('enable-native-gpu-memory-buffers')
|
||||
electron.app.commandLine.appendSwitch('enable-accelerated-video-decode');
|
||||
electron.app.commandLine.appendSwitch('enable-gpu-rasterization');
|
||||
electron.app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
|
||||
electron.app.commandLine.appendSwitch('enable-oop-rasterization');
|
||||
break;
|
||||
|
||||
case "webgpu":
|
||||
console.info("WebGPU is enabled.");
|
||||
electron.app.commandLine.appendSwitch('enable-unsafe-webgpu')
|
||||
break;
|
||||
|
||||
case "disabled":
|
||||
console.info("Hardware acceleration is disabled.");
|
||||
electron.app.commandLine.appendSwitch('disable-gpu')
|
||||
break;
|
||||
}
|
||||
|
||||
/***********************************************************************************************************************
|
||||
* Protocols
|
||||
**********************************************************************************************************************/
|
||||
if (process.defaultApp) {
|
||||
if (process.argv.length >= 2) {
|
||||
this.protocols.forEach((protocol: string) => {
|
||||
electron.app.setAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])])
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.protocols.forEach((protocol: string) => {
|
||||
electron.app.setAsDefaultProtocolClient(protocol)
|
||||
})
|
||||
}
|
||||
|
||||
electron.app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
if (this.protocols.some((protocol: string) => url.includes(protocol))) {
|
||||
AppEvents.LinkHandler(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public quit() {
|
||||
console.log('App stopped');
|
||||
}
|
||||
|
||||
public ready() {
|
||||
console.log('App ready');
|
||||
}
|
||||
|
||||
/***********************************************************************************************************************
|
||||
* Private methods
|
||||
**********************************************************************************************************************/
|
||||
|
||||
private static LinkHandler(arg: string) {
|
||||
if (!arg) return;
|
||||
|
||||
// LastFM Auth URL
|
||||
if (arg.includes('auth')) {
|
||||
let authURI = String(arg).split('/auth/')[1]
|
||||
if (authURI.startsWith('lastfm')) { // If we wanted more auth options
|
||||
const authKey = authURI.split('lastfm?token=')[1];
|
||||
AppEvents.store.set('lastfm.enabled', true);
|
||||
AppEvents.store.set('lastfm.auth_token', authKey);
|
||||
// AppEvents.window.webContents.send('LastfmAuthenticated', authKey);
|
||||
// lastfm.authenticate()
|
||||
}
|
||||
}
|
||||
// Play
|
||||
else if (arg.includes('/play/')) { //Steer away from protocol:// specific conditionals
|
||||
|
||||
const playParam = arg.split('/play/')[1]
|
||||
if (playParam.includes('s/')) { // setQueue can be done with album, song, url, playlist id
|
||||
console.log(playParam)
|
||||
let song = playParam.split('s/')[1]
|
||||
console.warn(`[LinkHandler] Attempting to load song by id: ${song}`);
|
||||
// AppEvents.window.webContents.executeJavaScript(`
|
||||
// MusicKit.getInstance().setQueue({ song: '${song}'}).then(function(queue) {
|
||||
// MusicKit.getInstance().play();
|
||||
// });
|
||||
// `)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
src/main/base/plugins.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as electron from 'electron'
|
||||
|
||||
export default class PluginHandler {
|
||||
private basePluginsPath = path.join(__dirname, '../plugins');
|
||||
private userPluginsPath = path.join(electron.app.getPath('userData'), 'plugins');
|
||||
private pluginsList: any = {};
|
||||
|
||||
constructor() {
|
||||
|
||||
this.pluginsList = this.getPlugins();
|
||||
}
|
||||
|
||||
public getPlugins(): any {
|
||||
let plugins: any = {};
|
||||
|
||||
|
||||
if (fs.existsSync(this.basePluginsPath)) {
|
||||
fs.readdirSync(this.basePluginsPath).forEach(file => {
|
||||
if (file.endsWith('.ts') || file.endsWith('.js')) {
|
||||
const plugin = require(path.join(this.basePluginsPath, file)).default;
|
||||
if (plugins[file] || plugin.name in plugins) {
|
||||
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
|
||||
} else {
|
||||
plugins[file] = new plugin(electron.app);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (fs.existsSync(this.userPluginsPath)) {
|
||||
fs.readdirSync(this.userPluginsPath).forEach(file => {
|
||||
if (file.endsWith('.ts') || file.endsWith('.js')) {
|
||||
const plugin = require(path.join(this.userPluginsPath, file)).default;
|
||||
if (plugins[file] || plugin in plugins) {
|
||||
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
|
||||
} else {
|
||||
plugins[file] = new plugin(electron.app);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log('loaded plugins:', JSON.stringify(plugins))
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public callPlugins(event: string, ...args: any[]) {
|
||||
for (const plugin in this.pluginsList) {
|
||||
if (this.pluginsList[plugin][event]) {
|
||||
this.pluginsList[plugin][event](...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
130
src/main/base/store.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
import * as Store from 'electron-store';
|
||||
import * as electron from "electron";
|
||||
|
||||
export class ConfigStore {
|
||||
public store: Store | undefined;
|
||||
|
||||
private defaults: any = {
|
||||
"general": {
|
||||
"close_behavior": 0, // 0 = close, 1 = minimize, 2 = minimize to tray
|
||||
"startup_behavior": 0, // 0 = nothing, 1 = open on startup
|
||||
"discord_rpc": 1, // 0 = disabled, 1 = enabled as Cider, 2 = enabled as Apple Music
|
||||
"discordClearActivityOnPause": 1 // 0 = disabled, 1 = enabled
|
||||
},
|
||||
"home": {
|
||||
"followedArtists": [],
|
||||
"favoriteItems": []
|
||||
},
|
||||
"libraryPrefs": {
|
||||
"songs": {
|
||||
"sort": "name",
|
||||
"sortOrder": "asc",
|
||||
"size": "normal"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"volume": 1,
|
||||
"quality": "990",
|
||||
"seamless_audio": true,
|
||||
"normalization": false,
|
||||
"spatial": false,
|
||||
"spatial_properties": {
|
||||
"presets": [],
|
||||
"gain": 0.8,
|
||||
"listener_position": [0, 0, 0],
|
||||
"audio_position": [0, 0, 0],
|
||||
"room_dimensions": {
|
||||
"width": 32,
|
||||
"height": 12,
|
||||
"depth": 32
|
||||
},
|
||||
"room_materials": {
|
||||
"left": 'metal',
|
||||
"right": 'metal',
|
||||
"front": 'brick-bare',
|
||||
"back": 'brick-bare',
|
||||
"down": 'acoustic-ceiling-tiles',
|
||||
"up": 'acoustic-ceiling-tiles',
|
||||
}
|
||||
}
|
||||
},
|
||||
"visual": {
|
||||
"theme": "",
|
||||
"scrollbars": 0, // 0 = show on hover, 2 = always hide, 3 = always show
|
||||
"refresh_rate": 0,
|
||||
"animated_artwork": "limited", // 0 = always, 1 = limited, 2 = never
|
||||
"animated_artwork_qualityLevel": 1,
|
||||
"bg_artwork_rotation": false,
|
||||
"hw_acceleration": "default" // default, webgpu, disabled
|
||||
},
|
||||
"lyrics": {
|
||||
"enable_mxm": false,
|
||||
"mxm_karaoke": false,
|
||||
"mxm_language": "en",
|
||||
"enable_yt": false,
|
||||
},
|
||||
"lastfm": {
|
||||
"enabled": false,
|
||||
"scrobble_after": 30,
|
||||
"auth_token": "",
|
||||
"enabledRemoveFeaturingArtists": true,
|
||||
"NowPlaying": "true"
|
||||
},
|
||||
"advanced": {
|
||||
"AudioContext": false,
|
||||
"experiments": []
|
||||
}
|
||||
}
|
||||
private migrations: any = {}
|
||||
|
||||
constructor() {
|
||||
this.store = new Store({
|
||||
name: 'cider-config',
|
||||
defaults: this.defaults,
|
||||
migrations: this.migrations,
|
||||
});
|
||||
|
||||
this.store.set(this.mergeStore(this.defaults, this.store.store))
|
||||
this.ipcHandler(this.store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge Configurations
|
||||
* @param target The target configuration
|
||||
* @param source The source configuration
|
||||
*/
|
||||
private mergeStore = (target: { [x: string]: any; }, source: { [x: string]: any; }) => {
|
||||
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
|
||||
for (const key of Object.keys(source)) {
|
||||
if (key.includes('migrations')) {
|
||||
continue;
|
||||
}
|
||||
if (source[key] instanceof Object) Object.assign(source[key], this.mergeStore(target[key], source[key]))
|
||||
}
|
||||
// Join `target` and modified `source`
|
||||
Object.assign(target || {}, source)
|
||||
return target
|
||||
}
|
||||
|
||||
/**
|
||||
* IPC Handler
|
||||
*/
|
||||
private ipcHandler(cfg: Store | any): void {
|
||||
electron.ipcMain.handle('getStoreValue', (event, key, defaultValue) => {
|
||||
return (defaultValue ? cfg.get(key, true) : cfg.get(key));
|
||||
});
|
||||
|
||||
electron.ipcMain.handle('setStoreValue', (event, key, value) => {
|
||||
cfg.set(key, value);
|
||||
});
|
||||
|
||||
electron.ipcMain.on('getStore', (event) => {
|
||||
event.returnValue = cfg.store
|
||||
})
|
||||
|
||||
electron.ipcMain.on('setStore', (event, store) => {
|
||||
cfg.store = store
|
||||
})
|
||||
}
|
||||
|
||||
}
|
443
src/main/base/win.ts
Normal file
|
@ -0,0 +1,443 @@
|
|||
// @ts-nocheck
|
||||
import * as path from "path";
|
||||
import * as electron from "electron";
|
||||
import * as windowStateKeeper from "electron-window-state";
|
||||
import * as express from "express";
|
||||
import * as getPort from "get-port";
|
||||
import * as yt from "youtube-search-without-api-key";
|
||||
import * as fs from "fs";
|
||||
import { Stream } from "stream";
|
||||
import * as qrcode from "qrcode-terminal";
|
||||
import * as os from "os";
|
||||
import {wsapi} from "./wsapi";
|
||||
|
||||
export class Win {
|
||||
win: any | undefined = null;
|
||||
app: any | undefined = null;
|
||||
store: any | undefined = null;
|
||||
devMode: boolean = !electron.app.isPackaged;
|
||||
|
||||
constructor(app: electron.App, store: any) {
|
||||
this.app = app;
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
private paths: any = {
|
||||
srcPath: path.join(__dirname, "../../src"),
|
||||
resourcePath: path.join(__dirname, "../../resources"),
|
||||
ciderCache: path.resolve(electron.app.getPath("userData"), "CiderCache"),
|
||||
themes: path.resolve(electron.app.getPath("userData"), "Themes"),
|
||||
plugins: path.resolve(electron.app.getPath("userData"), "Plugins"),
|
||||
};
|
||||
private audioStream: any = new Stream.PassThrough();
|
||||
private clientPort: number = 0;
|
||||
private remotePort: number = 6942;
|
||||
private EnvironmentVariables: object = {
|
||||
env: {
|
||||
platform: process.platform,
|
||||
dev: electron.app.isPackaged,
|
||||
},
|
||||
};
|
||||
private options: any = {
|
||||
icon: path.join(
|
||||
this.paths.resourcePath,
|
||||
`icons/icon.` + (process.platform === "win32" ? "ico" : "png")
|
||||
),
|
||||
width: 1024,
|
||||
height: 600,
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
minWidth: 844,
|
||||
minHeight: 410,
|
||||
frame: false,
|
||||
title: "Cider",
|
||||
vibrancy: "dark",
|
||||
transparent: process.platform === "darwin",
|
||||
hasShadow: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
sandbox: true,
|
||||
allowRunningInsecureContent: true,
|
||||
contextIsolation: false,
|
||||
|
||||
webviewTag: true,
|
||||
plugins: true,
|
||||
nodeIntegrationInWorker: false,
|
||||
webSecurity: false,
|
||||
|
||||
preload: path.join(this.paths.srcPath, "./preload/cider-preload.js"),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the browser window
|
||||
*/
|
||||
async createWindow(): Promise<void> {
|
||||
this.clientPort = await getPort({ port: 9000 });
|
||||
this.verifyFiles();
|
||||
|
||||
// Load the previous state with fallback to defaults
|
||||
const windowState = windowStateKeeper({
|
||||
defaultWidth: 1024,
|
||||
defaultHeight: 600,
|
||||
});
|
||||
this.options.width = windowState.width;
|
||||
this.options.height = windowState.height;
|
||||
|
||||
// Start the webserver for the browser window to load
|
||||
const ws = new wsapi()
|
||||
ws.InitWebSockets()
|
||||
this.startWebServer();
|
||||
|
||||
this.win = new electron.BrowserWindow(this.options);
|
||||
|
||||
// and load the renderer.
|
||||
this.startSession();
|
||||
this.startHandlers();
|
||||
|
||||
// Register listeners on Window to track size and position of the Window.
|
||||
windowState.manage(this.win);
|
||||
|
||||
return this.win;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the files for the renderer to use (Cache, library info, etc.)
|
||||
*/
|
||||
private verifyFiles(): void {
|
||||
const expectedDirectories = ["CiderCache"];
|
||||
const expectedFiles = [
|
||||
"library-songs.json",
|
||||
"library-artists.json",
|
||||
"library-albums.json",
|
||||
"library-playlists.json",
|
||||
"library-recentlyAdded.json",
|
||||
];
|
||||
for (let i = 0; i < expectedDirectories.length; i++) {
|
||||
if (
|
||||
!fs.existsSync(
|
||||
path.join(electron.app.getPath("userData"), expectedDirectories[i])
|
||||
)
|
||||
) {
|
||||
fs.mkdirSync(
|
||||
path.join(electron.app.getPath("userData"), expectedDirectories[i])
|
||||
);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < expectedFiles.length; i++) {
|
||||
const file = path.join(this.paths.ciderCache, expectedFiles[i]);
|
||||
if (!fs.existsSync(file)) {
|
||||
fs.writeFileSync(file, JSON.stringify([]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the webserver for the renderer process.
|
||||
*/
|
||||
private startWebServer(): void {
|
||||
const app = express();
|
||||
|
||||
app.use(express.static(path.join(this.paths.srcPath, "./renderer/")));
|
||||
app.set("views", path.join(this.paths.srcPath, "./renderer/views"));
|
||||
app.set("view engine", "ejs");
|
||||
let firstRequest = true;
|
||||
app.use((req, res, next) => {
|
||||
// @ts-ignore
|
||||
if (
|
||||
req.url.includes("audio.webm") ||
|
||||
(req.headers.host.includes("localhost") &&
|
||||
(this.devMode || req.headers["user-agent"].includes("Electron")))
|
||||
) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect("https://discord.gg/applemusic");
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
|
||||
|
||||
res.render("main", this.EnvironmentVariables);
|
||||
});
|
||||
|
||||
app.get("/audio.webm", (req, res) => {
|
||||
try {
|
||||
req.socket.setTimeout(Number.MAX_SAFE_INTEGER);
|
||||
// CiderBase.requests.push({req: req, res: res});
|
||||
// var pos = CiderBase.requests.length - 1;
|
||||
// req.on("close", () => {
|
||||
// console.info("CLOSED", CiderBase.requests.length);
|
||||
// requests.splice(pos, 1);
|
||||
// console.info("CLOSED", CiderBase.requests.length);
|
||||
// });
|
||||
this.audioStream.on("data", (data: any) => {
|
||||
try {
|
||||
res.write(data);
|
||||
} catch (ex) {
|
||||
console.log(ex);
|
||||
}
|
||||
});
|
||||
} catch (ex) {
|
||||
console.log(ex);
|
||||
}
|
||||
});
|
||||
//app.use(express.static())
|
||||
|
||||
app.listen(this.clientPort, () => {
|
||||
console.log(`Cider client port: ${this.clientPort}`);
|
||||
});
|
||||
|
||||
/*
|
||||
* Remote Client (I had no idea how to add it to our existing express server, so I just made another one) -@quacksire
|
||||
* TODO: Broadcast the remote so that /web-remote/ can connect
|
||||
* https://github.com/ciderapp/Apple-Music-Electron/blob/818ed18940ff600d76eb59d22016723a75885cd5/resources/functions/handler.js#L1173
|
||||
*/
|
||||
const remote = express();
|
||||
remote.use(express.static(path.join(this.paths.srcPath, "./web-remote/")))
|
||||
remote.listen(this.remotePort, () => {
|
||||
console.log(`Cider remote port: ${this.remotePort}`);
|
||||
if (firstRequest) {
|
||||
console.log("---- Ignore Me ;) ---");
|
||||
qrcode.generate(`http://${os.hostname}:${this.remotePort}`);
|
||||
console.log("---- Ignore Me ;) ---");
|
||||
/*
|
||||
*
|
||||
* USING https://www.npmjs.com/package/qrcode-terminal for terminal
|
||||
* WE SHOULD USE https://www.npmjs.com/package/qrcode for the remote (or others) for showing to user via an in-app dialog
|
||||
* -@quacksire
|
||||
*/
|
||||
}
|
||||
firstRequest = false;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the session for the renderer process.
|
||||
*/
|
||||
private startSession(): void {
|
||||
// intercept "https://js-cdn.music.apple.com/hls.js/2.141.0/hls.js/hls.js" and redirect to local file "./apple-hls.js" instead
|
||||
this.win.webContents.session.webRequest.onBeforeRequest(
|
||||
{
|
||||
urls: ["https://*/*.js"],
|
||||
},
|
||||
(
|
||||
details: { url: string | string[] },
|
||||
callback: (arg0: { redirectURL?: string; cancel?: boolean }) => void
|
||||
) => {
|
||||
if (details.url.includes("hls.js")) {
|
||||
callback({
|
||||
redirectURL: `http://localhost:${this.clientPort}/apple-hls.js`,
|
||||
});
|
||||
} else {
|
||||
callback({
|
||||
cancel: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.win.webContents.session.webRequest.onBeforeSendHeaders(
|
||||
async (
|
||||
details: { url: string; requestHeaders: { [x: string]: string } },
|
||||
callback: (arg0: { requestHeaders: any }) => void
|
||||
) => {
|
||||
if (details.url === "https://buy.itunes.apple.com/account/web/info") {
|
||||
details.requestHeaders["sec-fetch-site"] = "same-site";
|
||||
details.requestHeaders["DNT"] = "1";
|
||||
let itspod = await this.win.webContents.executeJavaScript(
|
||||
`window.localStorage.getItem("music.ampwebplay.itspod")`
|
||||
);
|
||||
if (itspod != null)
|
||||
details.requestHeaders["Cookie"] = `itspod=${itspod}`;
|
||||
}
|
||||
callback({ requestHeaders: details.requestHeaders });
|
||||
}
|
||||
);
|
||||
|
||||
let location = `http://localhost:${this.clientPort}/`;
|
||||
|
||||
if (electron.app.isPackaged) {
|
||||
this.win.loadURL(location);
|
||||
} else {
|
||||
this.win.loadURL(location, {
|
||||
userAgent: "Cider Development Environment",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the window handlers
|
||||
*/
|
||||
private startHandlers(): void {
|
||||
/**********************************************************************************************************************
|
||||
* ipcMain Events
|
||||
****************************************************************************************************************** */
|
||||
electron.ipcMain.on("cider-platform", (event) => {
|
||||
event.returnValue = process.platform;
|
||||
});
|
||||
|
||||
electron.ipcMain.on("get-gpu-mode", (event) => {
|
||||
event.returnValue = process.platform;
|
||||
});
|
||||
|
||||
electron.ipcMain.on("is-dev", (event) => {
|
||||
event.returnValue = this.devMode;
|
||||
});
|
||||
|
||||
electron.ipcMain.on("close", () => {
|
||||
// listen for close event
|
||||
this.win.close();
|
||||
});
|
||||
|
||||
electron.ipcMain.on("put-library-songs", (event, arg) => {
|
||||
fs.writeFileSync(
|
||||
path.join(this.paths.ciderCache, "library-songs.json"),
|
||||
JSON.stringify(arg)
|
||||
);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("put-library-artists", (event, arg) => {
|
||||
fs.writeFileSync(
|
||||
path.join(this.paths.ciderCache, "library-artists.json"),
|
||||
JSON.stringify(arg)
|
||||
);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("put-library-albums", (event, arg) => {
|
||||
fs.writeFileSync(
|
||||
path.join(this.paths.ciderCache, "library-albums.json"),
|
||||
JSON.stringify(arg)
|
||||
);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("put-library-playlists", (event, arg) => {
|
||||
fs.writeFileSync(
|
||||
path.join(this.paths.ciderCache, "library-playlists.json"),
|
||||
JSON.stringify(arg)
|
||||
);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("put-library-recentlyAdded", (event, arg) => {
|
||||
fs.writeFileSync(
|
||||
path.join(this.paths.ciderCache, "library-recentlyAdded.json"),
|
||||
JSON.stringify(arg)
|
||||
);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("get-library-songs", (event) => {
|
||||
let librarySongs = fs.readFileSync(
|
||||
path.join(this.paths.ciderCache, "library-songs.json"),
|
||||
"utf8"
|
||||
);
|
||||
event.returnValue = JSON.parse(librarySongs);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("get-library-artists", (event) => {
|
||||
let libraryArtists = fs.readFileSync(
|
||||
path.join(this.paths.ciderCache, "library-artists.json"),
|
||||
"utf8"
|
||||
);
|
||||
event.returnValue = JSON.parse(libraryArtists);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("get-library-albums", (event) => {
|
||||
let libraryAlbums = fs.readFileSync(
|
||||
path.join(this.paths.ciderCache, "library-albums.json"),
|
||||
"utf8"
|
||||
);
|
||||
event.returnValue = JSON.parse(libraryAlbums);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("get-library-playlists", (event) => {
|
||||
let libraryPlaylists = fs.readFileSync(
|
||||
path.join(this.paths.ciderCache, "library-playlists.json"),
|
||||
"utf8"
|
||||
);
|
||||
event.returnValue = JSON.parse(libraryPlaylists);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("get-library-recentlyAdded", (event) => {
|
||||
let libraryRecentlyAdded = fs.readFileSync(
|
||||
path.join(this.paths.ciderCache, "library-recentlyAdded.json"),
|
||||
"utf8"
|
||||
);
|
||||
event.returnValue = JSON.parse(libraryRecentlyAdded);
|
||||
});
|
||||
|
||||
electron.ipcMain.handle("getYTLyrics", async (event, track, artist) => {
|
||||
const u = track + " " + artist + " official video";
|
||||
return await yt.search(u);
|
||||
});
|
||||
|
||||
electron.ipcMain.handle("setVibrancy", (event, key, value) => {
|
||||
this.win.setVibrancy(value);
|
||||
});
|
||||
|
||||
electron.ipcMain.on("maximize", () => {
|
||||
// listen for maximize event
|
||||
if (this.win.isMaximized()) {
|
||||
this.win.unmaximize();
|
||||
} else {
|
||||
this.win.maximize();
|
||||
}
|
||||
});
|
||||
|
||||
electron.ipcMain.on("minimize", () => {
|
||||
// listen for minimize event
|
||||
this.win.minimize();
|
||||
});
|
||||
|
||||
// Set scale
|
||||
electron.ipcMain.on("setScreenScale", (event, scale) => {
|
||||
this.win.webContents.setZoomFactor(parseFloat(scale));
|
||||
});
|
||||
|
||||
/* *********************************************************************************************
|
||||
* Window Events
|
||||
* **********************************************************************************************/
|
||||
|
||||
if (process.platform === "win32") {
|
||||
let WND_STATE = {
|
||||
MINIMIZED: 0,
|
||||
NORMAL: 1,
|
||||
MAXIMIZED: 2,
|
||||
FULL_SCREEN: 3,
|
||||
};
|
||||
let wndState = WND_STATE.NORMAL;
|
||||
|
||||
this.win.on("resize", (_: any) => {
|
||||
const isMaximized = this.win.isMaximized();
|
||||
const isMinimized = this.win.isMinimized();
|
||||
const isFullScreen = this.win.isFullScreen();
|
||||
const state = wndState;
|
||||
if (isMinimized && state !== WND_STATE.MINIMIZED) {
|
||||
wndState = WND_STATE.MINIMIZED;
|
||||
} else if (isFullScreen && state !== WND_STATE.FULL_SCREEN) {
|
||||
wndState = WND_STATE.FULL_SCREEN;
|
||||
} else if (isMaximized && state !== WND_STATE.MAXIMIZED) {
|
||||
wndState = WND_STATE.MAXIMIZED;
|
||||
this.win.webContents.executeJavaScript(`app.chrome.maximized = true`);
|
||||
} else if (state !== WND_STATE.NORMAL) {
|
||||
wndState = WND_STATE.NORMAL;
|
||||
this.win.webContents.executeJavaScript(
|
||||
`app.chrome.maximized = false`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.win.on("closed", () => {
|
||||
this.win = null;
|
||||
});
|
||||
|
||||
// Set window Handler
|
||||
this.win.webContents.setWindowOpenHandler((x: any) => {
|
||||
if (x.url.includes("apple") || x.url.includes("localhost")) {
|
||||
return { action: "allow" };
|
||||
}
|
||||
electron.shell.openExternal(x.url).catch(console.error);
|
||||
return { action: "deny" };
|
||||
});
|
||||
}
|
||||
}
|
284
src/main/base/wsapi.ts
Normal file
|
@ -0,0 +1,284 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import * as ws from "ws";
|
||||
import * as http from "http";
|
||||
import * as https from "https";
|
||||
import * as url from "url";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as electron from "electron";
|
||||
const WebSocket = ws;
|
||||
const WebSocketServer = ws.Server;
|
||||
|
||||
private class standardResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
data: any;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class wsapi {
|
||||
port: any = 26369
|
||||
wss: any = null
|
||||
clients: []
|
||||
createId() {
|
||||
// create random guid
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0,
|
||||
v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
public async InitWebSockets () {
|
||||
electron.ipcMain.on('wsapi-updatePlaybackState', (event, arg) => {
|
||||
wsapi.updatePlaybackState(arg);
|
||||
})
|
||||
|
||||
electron.ipcMain.on('wsapi-returnQueue', (event, arg) => {
|
||||
wsapi.returnQueue(JSON.parse(arg));
|
||||
});
|
||||
|
||||
electron.ipcMain.on('wsapi-returnSearch', (event, arg) => {
|
||||
console.log("SEARCH")
|
||||
wsapi.returnSearch(JSON.parse(arg));
|
||||
});
|
||||
|
||||
electron.ipcMain.on('wsapi-returnSearchLibrary', (event, arg) => {
|
||||
wsapi.returnSearchLibrary(JSON.parse(arg));
|
||||
});
|
||||
|
||||
electron.ipcMain.on('wsapi-returnDynamic', (event, arg, type) => {
|
||||
wsapi.returnDynamic(JSON.parse(arg), type);
|
||||
});
|
||||
|
||||
electron.ipcMain.on('wsapi-returnMusicKitApi', (event, arg, method) => {
|
||||
wsapi.returnMusicKitApi(JSON.parse(arg), method);
|
||||
});
|
||||
|
||||
electron.ipcMain.on('wsapi-returnLyrics', (event, arg) => {
|
||||
wsapi.returnLyrics(JSON.parse(arg));
|
||||
});
|
||||
this.wss = new WebSocketServer({
|
||||
port: this.port,
|
||||
perMessageDeflate: {
|
||||
zlibDeflateOptions: {
|
||||
// See zlib defaults.
|
||||
chunkSize: 1024,
|
||||
memLevel: 7,
|
||||
level: 3
|
||||
},
|
||||
zlibInflateOptions: {
|
||||
chunkSize: 10 * 1024
|
||||
},
|
||||
// Other options settable:
|
||||
clientNoContextTakeover: true, // Defaults to negotiated value.
|
||||
serverNoContextTakeover: true, // Defaults to negotiated value.
|
||||
serverMaxWindowBits: 10, // Defaults to negotiated value.
|
||||
// Below options specified as default values.
|
||||
concurrencyLimit: 10, // Limits zlib concurrency for perf.
|
||||
threshold: 1024 // Size (in bytes) below which messages
|
||||
// should not be compressed if context takeover is disabled.
|
||||
}
|
||||
})
|
||||
console.log(`WebSocketServer started on port: ${this.port}`);
|
||||
|
||||
const defaultResponse = new standardResponse(0, {}, "OK");
|
||||
|
||||
|
||||
this.wss.on('connection', function connection(ws) {
|
||||
ws.id = wsapi.createId();
|
||||
console.log(`Client ${ws.id} connected`)
|
||||
wsapi.clients.push(ws);
|
||||
ws.on('message', function incoming(message) {
|
||||
|
||||
});
|
||||
// ws on message
|
||||
ws.on('message', function incoming(message) {
|
||||
let data = JSON.parse(message);
|
||||
let response = new standardResponse(0, {}, "OK");;
|
||||
if (data.action) {
|
||||
data.action.toLowerCase();
|
||||
}
|
||||
switch (data.action) {
|
||||
default:
|
||||
response.message = "Action not found";
|
||||
break;
|
||||
case "identify":
|
||||
response.message = "Thanks for identifying!"
|
||||
response.data = {
|
||||
id: ws.id
|
||||
}
|
||||
ws.identity = {
|
||||
name: data.name,
|
||||
author: data.author,
|
||||
description: data.description,
|
||||
version: data.version
|
||||
}
|
||||
break;
|
||||
case "play-next":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.playNext(\`${data.type}\`,\`${data.id}\`)`);
|
||||
response.message = "Play Next";
|
||||
break;
|
||||
case "play-later":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.playLater(\`${data.type}\`,\`${data.id}\`)`);
|
||||
response.message = "Play Later";
|
||||
break;
|
||||
case "quick-play":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.quickPlay(\`${data.term}\`)`);
|
||||
response.message = "Quick Play";
|
||||
break;
|
||||
case "get-lyrics":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.getLyrics()`);
|
||||
break;
|
||||
case "shuffle":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.toggleShuffle()`);
|
||||
break;
|
||||
case "set-shuffle":
|
||||
if(data.shuffle == true) {
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 1`);
|
||||
}else{
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 0`);
|
||||
}
|
||||
break;
|
||||
case "repeat":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.toggleRepeat()`);
|
||||
break;
|
||||
case "seek":
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${parseFloat(data.time)})`);
|
||||
response.message = "Seek";
|
||||
break;
|
||||
case "pause":
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().pause()`);
|
||||
response.message = "Paused";
|
||||
break;
|
||||
case "play":
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().play()`);
|
||||
response.message = "Playing";
|
||||
break;
|
||||
case "stop":
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().stop()`);
|
||||
response.message = "Stopped";
|
||||
break;
|
||||
case "volume":
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().volume = ${parseFloat(data.volume)}`);
|
||||
response.message = "Volume";
|
||||
break;
|
||||
case "mute":
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().mute()`);
|
||||
response.message = "Muted";
|
||||
break;
|
||||
case "unmute":
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().unmute()`);
|
||||
response.message = "Unmuted";
|
||||
break;
|
||||
case "next":
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().skipToNextItem()`);
|
||||
response.message = "Next";
|
||||
break;
|
||||
case "previous":
|
||||
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().skipToPreviousItem()`);
|
||||
response.message = "Previous";
|
||||
break;
|
||||
case "musickit-api":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.musickitApi(\`${data.method}\`, \`${data.id}\`, ${JSON.stringify(data.params)})`);
|
||||
break;
|
||||
case "musickit-library-api":
|
||||
break;
|
||||
case "set-autoplay":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.setAutoplay(${data.autoplay})`);
|
||||
break;
|
||||
case "queue-move":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.moveQueueItem(${data.from},${data.to})`);
|
||||
break;
|
||||
case "get-queue":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.getQueue()`);
|
||||
break;
|
||||
case "search":
|
||||
if (!data.limit) {
|
||||
data.limit = 10;
|
||||
}
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.search(\`${data.term}\`, \`${data.limit}\`)`);
|
||||
break;
|
||||
case "library-search":
|
||||
if (!data.limit) {
|
||||
data.limit = 10;
|
||||
}
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.searchLibrary(\`${data.term}\`, \`${data.limit}\`)`);
|
||||
break;
|
||||
case "show-window":
|
||||
electron.app.win.show()
|
||||
break;
|
||||
case "hide-window":
|
||||
electron.app.win.hide()
|
||||
break;
|
||||
case "play-mediaitem":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.playTrackById(${data.id}, \`${data.kind}\`)`);
|
||||
response.message = "Playing track";
|
||||
break;
|
||||
case "get-status":
|
||||
response.data = {
|
||||
isAuthorized: true
|
||||
};
|
||||
response.message = "Status";
|
||||
break;
|
||||
case "get-currentmediaitem":
|
||||
electron.app.win.webContents.executeJavaScript(`wsapi.getPlaybackState()`);
|
||||
break;
|
||||
}
|
||||
ws.send(JSON.stringify(response));
|
||||
});
|
||||
|
||||
ws.on('close', function close() {
|
||||
// remove client from list
|
||||
wsapi.clients.splice(wsapi.clients.indexOf(ws), 1);
|
||||
console.log(`Client ${ws.id} disconnected`);
|
||||
});
|
||||
ws.send(JSON.stringify(defaultResponse));
|
||||
});
|
||||
}
|
||||
sendToClient(id) {
|
||||
// replace the clients.forEach with a filter to find the client that requested
|
||||
}
|
||||
updatePlaybackState(attr) {
|
||||
const response = new standardResponse(0, attr, "OK", "playbackStateUpdate");
|
||||
wsapi.clients.forEach(function each(client) {
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
returnMusicKitApi(results, method) {
|
||||
const response = new standardResponse(0, results, "OK", `musickitapi.${method}`);
|
||||
wsapi.clients.forEach(function each(client) {
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
returnDynamic(results, type) {
|
||||
const response = new standardResponse(0, results, "OK", type);
|
||||
wsapi.clients.forEach(function each(client) {
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
returnLyrics(results) {
|
||||
const response = new standardResponse(0, results, "OK", "lyrics");
|
||||
wsapi.clients.forEach(function each(client) {
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
returnSearch(results) {
|
||||
const response = new standardResponse(0, results, "OK", "searchResults");
|
||||
wsapi.clients.forEach(function each(client) {
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
returnSearchLibrary(results) {
|
||||
const response = new standardResponse(0, results, "OK", "searchResultsLibrary");
|
||||
wsapi.clients.forEach(function each(client) {
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
returnQueue(queue) {
|
||||
const response = new standardResponse(0, queue, "OK", "queue");
|
||||
wsapi.clients.forEach(function each(client) {
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,446 +0,0 @@
|
|||
const { BrowserWindow, ipcMain, shell, app, screen } = require("electron")
|
||||
const { join } = require("path")
|
||||
const getPort = require("get-port");
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const windowStateKeeper = require("electron-window-state");
|
||||
const os = require('os');
|
||||
const yt = require('youtube-search-without-api-key');
|
||||
const discord = require('./discordrpc');
|
||||
const lastfm = require('./lastfm');
|
||||
const { writeFile, writeFileSync, existsSync, mkdirSync } = require('fs');
|
||||
const fs = require('fs');
|
||||
const mpris = require('./mpris');
|
||||
const mm = require('music-metadata');
|
||||
//const mdns = require('mdns')
|
||||
const qrcode = require('qrcode-terminal')
|
||||
const fetch = require('electron-fetch').default;
|
||||
const { Stream } = require('stream');
|
||||
|
||||
// Analytics for debugging.
|
||||
const ElectronSentry = require("@sentry/electron");
|
||||
ElectronSentry.init({ dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214" });
|
||||
|
||||
const CiderBase = {
|
||||
win: null,
|
||||
requests: [],
|
||||
audiostream: new Stream.PassThrough(),
|
||||
async Start() {
|
||||
this.clientPort = await getPort({ port: 9000 });
|
||||
this.win = this.CreateBrowserWindow()
|
||||
},
|
||||
clientPort: 0,
|
||||
CreateBrowserWindow() {
|
||||
this.VerifyFiles()
|
||||
// Set default window sizes
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1024,
|
||||
defaultHeight: 600
|
||||
});
|
||||
|
||||
let win = null
|
||||
const options = {
|
||||
icon: join(__dirname, `../../resources/icons/icon.` + (process.platform === "win32" ? "ico" : "png")),
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
minWidth: 844,
|
||||
minHeight: 410,
|
||||
frame: false,
|
||||
title: "Cider",
|
||||
vibrancy: 'dark',
|
||||
// transparent: true,
|
||||
hasShadow: false,
|
||||
webPreferences: {
|
||||
webviewTag: true,
|
||||
plugins: true,
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: false,
|
||||
webSecurity: false,
|
||||
allowRunningInsecureContent: true,
|
||||
enableRemoteModule: true,
|
||||
sandbox: true,
|
||||
nativeWindowOpen: true,
|
||||
contextIsolation: false,
|
||||
preload: join(__dirname, '../preload/cider-preload.js')
|
||||
}
|
||||
}
|
||||
|
||||
CiderBase.InitWebServer()
|
||||
|
||||
// Create the BrowserWindow
|
||||
win = new BrowserWindow(options)
|
||||
|
||||
// intercept "https://js-cdn.music.apple.com/hls.js/2.141.0/hls.js/hls.js" and redirect to local file "./apple-hls.js" instead
|
||||
win.webContents.session.webRequest.onBeforeRequest({
|
||||
urls: ["https://*/*.js"]
|
||||
},
|
||||
(details, callback) => {
|
||||
if (details.url.includes("hls.js")) {
|
||||
callback({
|
||||
redirectURL: `http://localhost:${CiderBase.clientPort}/apple-hls.js`
|
||||
})
|
||||
} else {
|
||||
callback({
|
||||
cancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
win.webContents.session.webRequest.onBeforeSendHeaders(async(details, callback) => {
|
||||
if (details.url === "https://buy.itunes.apple.com/account/web/info") {
|
||||
details.requestHeaders['sec-fetch-site'] = 'same-site';
|
||||
details.requestHeaders['DNT'] = '1';
|
||||
let itspod = await win.webContents.executeJavaScript(`window.localStorage.getItem("music.ampwebplay.itspod")`)
|
||||
if (itspod != null)
|
||||
details.requestHeaders['Cookie'] = `itspod=${itspod}`
|
||||
}
|
||||
callback({ requestHeaders: details.requestHeaders })
|
||||
})
|
||||
|
||||
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
if (details.url.match(/^https:\/\/store-\d{3}\.blobstore\.apple\.com/) || details.url.startsWith("https://store-037.blobstore.apple.com")) {
|
||||
details.responseHeaders['Access-Control-Allow-Origin'] = '*';
|
||||
}
|
||||
callback({ responseHeaders: details.responseHeaders })
|
||||
})
|
||||
|
||||
let location = `http://localhost:${CiderBase.clientPort}/`
|
||||
win.loadURL(location)
|
||||
win.on("closed", () => {
|
||||
win = null
|
||||
})
|
||||
|
||||
// Register listeners on Window to track size and position of the Window.
|
||||
mainWindowState.manage(win);
|
||||
|
||||
// IPC stuff (senders)
|
||||
ipcMain.on("cider-platform", (event) => {
|
||||
event.returnValue = process.platform
|
||||
})
|
||||
|
||||
ipcMain.on("get-gpu-mode", (event) => {
|
||||
event.returnValue = process.platform
|
||||
})
|
||||
|
||||
ipcMain.on("is-dev", (event) => {
|
||||
event.returnValue = !app.isPackaged
|
||||
})
|
||||
|
||||
// IPC stuff (listeners)
|
||||
ipcMain.on('close', () => { // listen for close event
|
||||
win.close();
|
||||
})
|
||||
|
||||
ipcMain.on('put-library-songs', (event, arg) => {
|
||||
fs.writeFileSync(join(app.paths.ciderCache, "library-songs.json"), JSON.stringify(arg))
|
||||
})
|
||||
|
||||
ipcMain.on('put-library-artists', (event, arg) => {
|
||||
fs.writeFileSync(join(app.paths.ciderCache, "library-artists.json"), JSON.stringify(arg))
|
||||
})
|
||||
|
||||
ipcMain.on('put-library-albums', (event, arg) => {
|
||||
fs.writeFileSync(join(app.paths.ciderCache, "library-albums.json"), JSON.stringify(arg))
|
||||
})
|
||||
|
||||
ipcMain.on('put-library-playlists', (event, arg) => {
|
||||
fs.writeFileSync(join(app.paths.ciderCache, "library-playlists.json"), JSON.stringify(arg))
|
||||
})
|
||||
|
||||
ipcMain.on('put-library-recentlyAdded', (event, arg) => {
|
||||
fs.writeFileSync(join(app.paths.ciderCache, "library-recentlyAdded.json"), JSON.stringify(arg))
|
||||
})
|
||||
|
||||
ipcMain.on('get-library-songs', (event) => {
|
||||
let librarySongs = fs.readFileSync(join(app.paths.ciderCache, "library-songs.json"), "utf8")
|
||||
event.returnValue = JSON.parse(librarySongs)
|
||||
})
|
||||
|
||||
ipcMain.on('get-library-artists', (event) => {
|
||||
let libraryArtists = fs.readFileSync(join(app.paths.ciderCache, "library-artists.json"), "utf8")
|
||||
event.returnValue = JSON.parse(libraryArtists)
|
||||
})
|
||||
|
||||
ipcMain.on('get-library-albums', (event) => {
|
||||
let libraryAlbums = fs.readFileSync(join(app.paths.ciderCache, "library-albums.json"), "utf8")
|
||||
event.returnValue = JSON.parse(libraryAlbums)
|
||||
})
|
||||
|
||||
ipcMain.on('get-library-playlists', (event) => {
|
||||
let libraryPlaylists = fs.readFileSync(join(app.paths.ciderCache, "library-playlists.json"), "utf8")
|
||||
event.returnValue = JSON.parse(libraryPlaylists)
|
||||
})
|
||||
|
||||
ipcMain.on('get-library-recentlyAdded', (event) => {
|
||||
let libraryRecentlyAdded = fs.readFileSync(join(app.paths.ciderCache, "library-recentlyAdded.json"), "utf8")
|
||||
event.returnValue = JSON.parse(libraryRecentlyAdded)
|
||||
})
|
||||
|
||||
ipcMain.handle('getYTLyrics', async(event, track, artist) => {
|
||||
var u = track + " " + artist + " official video";
|
||||
const videos = await yt.search(u);
|
||||
return videos
|
||||
})
|
||||
|
||||
ipcMain.handle('getStoreValue', (event, key, defaultValue) => {
|
||||
return (defaultValue ? app.cfg.get(key, true) : app.cfg.get(key));
|
||||
});
|
||||
|
||||
ipcMain.handle('setStoreValue', (event, key, value) => {
|
||||
app.cfg.set(key, value);
|
||||
});
|
||||
|
||||
ipcMain.on('getStore', (event) => {
|
||||
event.returnValue = app.cfg.store
|
||||
})
|
||||
|
||||
ipcMain.on('setStore', (event, store) => {
|
||||
app.cfg.store = store
|
||||
})
|
||||
|
||||
ipcMain.handle('setVibrancy', (event, key, value) => {
|
||||
win.setVibrancy(value)
|
||||
});
|
||||
|
||||
ipcMain.on('maximize', () => { // listen for maximize event
|
||||
if (win.isMaximized()) {
|
||||
win.unmaximize()
|
||||
} else {
|
||||
win.maximize()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('minimize', () => { // listen for minimize event
|
||||
win.minimize();
|
||||
})
|
||||
|
||||
ipcMain.on('setFullScreen', (event, flag) => {
|
||||
win.setFullScreen(flag)
|
||||
})
|
||||
|
||||
if (process.platform === "win32") {
|
||||
let WND_STATE = {
|
||||
MINIMIZED: 0,
|
||||
NORMAL: 1,
|
||||
MAXIMIZED: 2,
|
||||
FULL_SCREEN: 3
|
||||
}
|
||||
let wndState = WND_STATE.NORMAL
|
||||
|
||||
win.on("resize", (_event) => {
|
||||
const isMaximized = win.isMaximized()
|
||||
const isMinimized = win.isMinimized()
|
||||
const isFullScreen = win.isFullScreen()
|
||||
const state = wndState;
|
||||
if (isMinimized && state !== WND_STATE.MINIMIZED) {
|
||||
wndState = WND_STATE.MINIMIZED
|
||||
} else if (isFullScreen && state !== WND_STATE.FULL_SCREEN) {
|
||||
wndState = WND_STATE.FULL_SCREEN
|
||||
} else if (isMaximized && state !== WND_STATE.MAXIMIZED) {
|
||||
wndState = WND_STATE.MAXIMIZED
|
||||
win.webContents.executeJavaScript(`app.chrome.maximized = true`)
|
||||
} else if (state !== WND_STATE.NORMAL) {
|
||||
wndState = WND_STATE.NORMAL
|
||||
win.webContents.executeJavaScript(`app.chrome.maximized = false`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Set window Handler
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url.includes("apple") || url.includes("localhost")) {
|
||||
return { action: "allow" }
|
||||
}
|
||||
shell.openExternal(url).catch(() => {})
|
||||
return {
|
||||
action: 'deny'
|
||||
}
|
||||
})
|
||||
|
||||
// Set scale
|
||||
ipcMain.on('setScreenScale', (event, scale) => {
|
||||
win.webContents.setZoomFactor(parseFloat(scale))
|
||||
})
|
||||
|
||||
win.webContents.setZoomFactor(screen.getPrimaryDisplay().scaleFactor)
|
||||
|
||||
mpris.connect(win)
|
||||
mpris.SetButtons(win, false)
|
||||
|
||||
lastfm.authenticate()
|
||||
// Discord
|
||||
discord.connect((app.cfg.get("general.discord_rpc") == 1) ? '911790844204437504' : '886578863147192350');
|
||||
ipcMain.on('playbackStateDidChange', (_event, a) => {
|
||||
app.media = a;
|
||||
discord.updateActivity(a)
|
||||
mpris.SetButtons(win, a)
|
||||
mpris.updateState(a)
|
||||
lastfm.scrobbleSong(a)
|
||||
lastfm.updateNowPlayingSong(a)
|
||||
});
|
||||
|
||||
ipcMain.on('nowPlayingItemDidChange', (_event, a) => {
|
||||
app.media = a;
|
||||
discord.updateActivity(a)
|
||||
mpris.SetButtons(win, a)
|
||||
mpris.updateAttributes(a)
|
||||
lastfm.scrobbleSong(a)
|
||||
lastfm.updateNowPlayingSong(a)
|
||||
});
|
||||
|
||||
ipcMain.on("getPreviewURL", (_event, url) => {
|
||||
fetch(url)
|
||||
.then(res => res.buffer())
|
||||
.then(async(buffer) => {
|
||||
try {
|
||||
const metadata = await mm.parseBuffer(buffer, 'audio/x-m4a');
|
||||
SoundCheckTag = metadata.native.iTunes[1].value
|
||||
win.webContents.send('SoundCheckTag', SoundCheckTag)
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
ipcMain.on('writeAudio', function(event, buffer) {
|
||||
CiderBase.audiostream.write(Buffer.from(buffer));
|
||||
})
|
||||
|
||||
return win
|
||||
},
|
||||
VerifyFiles() {
|
||||
const expectedDirectories = [
|
||||
"CiderCache"
|
||||
]
|
||||
const expectedFiles = [
|
||||
"library-songs.json",
|
||||
"library-artists.json",
|
||||
"library-albums.json",
|
||||
"library-playlists.json",
|
||||
"library-recentlyAdded.json",
|
||||
]
|
||||
for (let i = 0; i < expectedDirectories.length; i++) {
|
||||
if (!existsSync(path.join(app.getPath("userData"), expectedDirectories[i]))) {
|
||||
mkdirSync(path.join(app.getPath("userData"), expectedDirectories[i]))
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < expectedFiles.length; i++) {
|
||||
const file = path.join(app.paths.ciderCache, expectedFiles[i])
|
||||
if (!existsSync(file)) {
|
||||
writeFileSync(file, JSON.stringify([]))
|
||||
}
|
||||
}
|
||||
},
|
||||
EnvironmentVariables: {
|
||||
"env": {
|
||||
platform: os.platform(),
|
||||
dev: app.isPackaged
|
||||
}
|
||||
},
|
||||
LinkHandler: (startArgs) => {
|
||||
if (!startArgs) return;
|
||||
|
||||
if (String(startArgs).includes('auth')) {
|
||||
let authURI = String(startArgs).split('/auth/')[1]
|
||||
if (authURI.startsWith('lastfm')) { // If we wanted more auth options
|
||||
console.log("lfmtoken", String(startArgs))
|
||||
const authKey = authURI.split('lastfm?token=')[1];
|
||||
app.cfg.set('lastfm.enabled', true);
|
||||
app.cfg.set('lastfm.auth_token', authKey);
|
||||
CiderBase.win.webContents.send('LastfmAuthenticated', authKey);
|
||||
lastfm.authenticate()
|
||||
}
|
||||
} else {
|
||||
if (String(startArgs).includes('/play/')) { //Steer away from protocal:// specific conditionals
|
||||
const playParam = String(startArgs).split('/play/')[1]
|
||||
if (playParam.includes('s/')) { // setQueue can be done with album, song, url, playlist id
|
||||
console.log(playParam)
|
||||
let song = playParam.split('s/')[1]
|
||||
console.warn(`[LinkHandler] Attempting to load song by id: ${song}`);
|
||||
this.win.webContents.executeJavaScript(`
|
||||
MusicKit.getInstance().setQueue({ song: '${song}'}).then(function(queue) {
|
||||
MusicKit.getInstance().play();
|
||||
});
|
||||
`).catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async InitWebServer() {
|
||||
const webapp = express();
|
||||
const webRemotePath = path.join(__dirname, '../renderer/');
|
||||
webapp.set("views", path.join(webRemotePath, "views"));
|
||||
webapp.set("view engine", "ejs");
|
||||
let firstRequest = true
|
||||
//const webRemoteMDNS = mdns.createAdvertisement(mdns.tcp('https'), 9000, { name: "cider", domain: 'local' })
|
||||
//webRemoteMDNS.start()
|
||||
//* Prep for remote -quack
|
||||
webapp.use(function(req, res, next) {
|
||||
// if not localhost
|
||||
if (req.url.includes("audio.webm") || (req.headers.host.includes("localhost") && req.headers["user-agent"].includes("Cider"))) {
|
||||
next();
|
||||
|
||||
} else {
|
||||
console.log(req.get('host'))
|
||||
res.redirect("https://discord.gg/applemusic")
|
||||
}
|
||||
});
|
||||
webapp.use(express.static(webRemotePath));
|
||||
webapp.get('/', function(req, res) {
|
||||
//if (!req.headers["user-agent"].includes("Cider"))
|
||||
//res.sendFile(path.join(webRemotePath, 'index_old.html'));
|
||||
if (firstRequest) {
|
||||
console.log("---- Ignore Me ;) ---")
|
||||
qrcode.generate(`http://${os.hostname}:9000`) //Prep for remote
|
||||
console.log("---- Ignore Me ;) ---")
|
||||
/*
|
||||
*
|
||||
* USING https://www.npmjs.com/package/qrcode-terminal for terminal
|
||||
* WE SHOULD USE https://www.npmjs.com/package/qrcode for the remote (or others)
|
||||
* -quack
|
||||
*/
|
||||
}
|
||||
firstRequest = false
|
||||
|
||||
res.render("main", CiderBase.EnvironmentVariables)
|
||||
});
|
||||
webapp.get('/audio.webm', function(req, res) {
|
||||
console.log('hi')
|
||||
try {
|
||||
req.connection.setTimeout(Number.MAX_SAFE_INTEGER);
|
||||
// CiderBase.requests.push({req: req, res: res});
|
||||
// var pos = CiderBase.requests.length - 1;
|
||||
// req.on("close", () => {
|
||||
// console.info("CLOSED", CiderBase.requests.length);
|
||||
// requests.splice(pos, 1);
|
||||
// console.info("CLOSED", CiderBase.requests.length);
|
||||
// });
|
||||
CiderBase.audiostream.on('data', (data) => {
|
||||
try {
|
||||
res.write(data);
|
||||
} catch (ex) {
|
||||
console.log(ex)
|
||||
}
|
||||
})
|
||||
} catch (ex) { console.log(ex) }
|
||||
});
|
||||
webapp.listen(CiderBase.clientPort, function() {
|
||||
console.log(`Cider hosted on: ${CiderBase.clientPort}`);
|
||||
});
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
module.exports = CiderBase;
|
|
@ -1,142 +0,0 @@
|
|||
const { app } = require('electron'),
|
||||
DiscordRPC = require('discord-rpc')
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* Connects to Discord RPC
|
||||
* @param {string} clientId
|
||||
*/
|
||||
connect: function(clientId) {
|
||||
app.discord = { isConnected: false };
|
||||
if (app.cfg.get('general.discord_rpc') == 0 || app.discord.isConnected) return;
|
||||
|
||||
DiscordRPC.register(clientId) // Apparently needed for ask to join, join, spectate etc.
|
||||
const client = new DiscordRPC.Client({ transport: "ipc" });
|
||||
app.discord = Object.assign(client, { error: false, activityCache: null, isConnected: false });
|
||||
|
||||
// Login to Discord
|
||||
app.discord.login({ clientId })
|
||||
.then(() => {
|
||||
app.discord.isConnected = true;
|
||||
})
|
||||
.catch((e) => console.error(`[DiscordRPC][connect] ${e}`));
|
||||
|
||||
app.discord.on('ready', () => {
|
||||
console.log(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${client.user.username} (${client.user.id})`);
|
||||
})
|
||||
|
||||
// Handles Errors
|
||||
app.discord.on('error', err => {
|
||||
console.error(`[DiscordRPC] ${err}`);
|
||||
this.disconnect()
|
||||
app.discord.isConnected = false;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Disconnects from Discord RPC
|
||||
*/
|
||||
disconnect: function() {
|
||||
if (app.cfg.get('general.discord_rpc') == 0 || !app.discord.isConnected) return;
|
||||
|
||||
try {
|
||||
app.discord.destroy().then(() => {
|
||||
app.discord.isConnected = false;
|
||||
console.log('[DiscordRPC][disconnect] Disconnected from discord.')
|
||||
}).catch((e) => console.error(`[DiscordRPC][disconnect] ${e}`));
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the activity of the client
|
||||
* @param {object} attributes
|
||||
*/
|
||||
updateActivity: function(attributes) {
|
||||
if (app.cfg.get('general.discord_rpc') == 0) return;
|
||||
|
||||
if (!app.discord.isConnected) {
|
||||
app.discord.clearActivity().catch((e) => console.error(`[DiscordRPC][updateActivity] ${e}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('[DiscordRPC][updateActivity] Updating Discord Activity.')
|
||||
|
||||
const listenURL = `https://cider.sh/p?s&id=${attributes.playParams.id}` // cider://play/s/[id] (for song)
|
||||
//console.log(attributes)
|
||||
let ActivityObject = {
|
||||
details: attributes.name,
|
||||
state: `by ${attributes.artistName}`,
|
||||
startTimestamp: attributes.startTime,
|
||||
endTimestamp: attributes.endTime,
|
||||
largeImageKey: (attributes.artwork.url.replace('{w}', '1024').replace('{h}', '1024')) ?? 'cider',
|
||||
largeImageText: attributes.albumName,
|
||||
smallImageKey: (attributes.status ? 'play' : 'pause'),
|
||||
smallImageText: (attributes.status ? 'Playing' : 'Paused'),
|
||||
instance: true,
|
||||
buttons: [
|
||||
{ label: "Listen on Cider", url: listenURL },
|
||||
]
|
||||
};
|
||||
if (ActivityObject.largeImageKey == "" || ActivityObject.largeImageKey == null) {
|
||||
ActivityObject.largeImageKey = (app.cfg.get("general.discord_rpc") == 1) ? "cider" : "logo"
|
||||
}
|
||||
|
||||
// Remove the pause/play icon and test for clear activity on pause
|
||||
if (app.cfg.get('general.discordClearActivityOnPause') == 1) {
|
||||
delete ActivityObject.smallImageKey
|
||||
delete ActivityObject.smallImageText
|
||||
}
|
||||
|
||||
// Deletes the timestamp if its not greater than 0
|
||||
if (!((new Date(attributes.endTime)).getTime() > 0)) {
|
||||
delete ActivityObject.startTimestamp
|
||||
delete ActivityObject.endTimestamp
|
||||
}
|
||||
|
||||
// Artist check
|
||||
if (!attributes.artistName) {
|
||||
delete ActivityObject.state
|
||||
}
|
||||
|
||||
// Album text check
|
||||
if (!ActivityObject.largeImageText || ActivityObject.largeImageText.length < 2) {
|
||||
delete ActivityObject.largeImageText
|
||||
}
|
||||
|
||||
// Checks if the name is greater than 128 because some songs can be that long
|
||||
if (ActivityObject.details.length > 128) {
|
||||
ActivityObject.details = ActivityObject.details.substring(0, 125) + '...'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Check if its pausing (false) or playing (true)
|
||||
if (!attributes.status) {
|
||||
if (app.cfg.get('general.discordClearActivityOnPause') == 1) {
|
||||
app.discord.clearActivity().catch((e) => console.error(`[DiscordRPC][clearActivity] ${e}`));
|
||||
ActivityObject = null
|
||||
} else {
|
||||
delete ActivityObject.startTimestamp
|
||||
delete ActivityObject.endTimestamp
|
||||
ActivityObject.smallImageKey = 'pause'
|
||||
ActivityObject.smallImageText = 'Paused'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (ActivityObject && ActivityObject !== app.discord.activityCache && ActivityObject.details && ActivityObject.state) {
|
||||
try {
|
||||
// console.log(`[DiscordRPC][setActivity] Setting activity to ${JSON.stringify(ActivityObject)}`);
|
||||
app.discord.setActivity(ActivityObject)
|
||||
app.discord.activityCache = ActivityObject
|
||||
} catch (err) {
|
||||
console.error(`[DiscordRPC][setActivity] ${err}`)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
}
|
116
src/main/index.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
require('v8-compile-cache');
|
||||
|
||||
// Analytics for debugging fun yeah.
|
||||
const ElectronSentry = require("@sentry/electron");
|
||||
ElectronSentry.init({dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214"});
|
||||
|
||||
import * as electron from 'electron';
|
||||
import {Win} from "./base/win";
|
||||
import {ConfigStore} from "./base/store";
|
||||
import {AppEvents} from "./base/app";
|
||||
import PluginHandler from "./base/plugins";
|
||||
|
||||
// const test = new PluginHandler();
|
||||
const config = new ConfigStore();
|
||||
const App = new AppEvents(config.store);
|
||||
const Cider = new Win(electron.app, config.store)
|
||||
const plug = new PluginHandler();
|
||||
|
||||
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* App Event Handlers
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
|
||||
|
||||
electron.app.on('ready', () => {
|
||||
App.ready();
|
||||
|
||||
console.log('[Cider] Application is Ready. Creating Window.')
|
||||
if (!electron.app.isPackaged) {
|
||||
console.info('[Cider] Running in development mode.')
|
||||
require('vue-devtools').install()
|
||||
}
|
||||
|
||||
electron.components.whenReady().then(() => {
|
||||
Cider.createWindow().then((win) => {
|
||||
plug.callPlugins('onReady', win);
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
});
|
||||
|
||||
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* Renderer Event Handlers
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
|
||||
|
||||
electron.ipcMain.on('playbackStateDidChange', (event, attributes) => {
|
||||
plug.callPlugins('onPlaybackStateDidChange', attributes);
|
||||
});
|
||||
|
||||
electron.ipcMain.on('nowPlayingItemDidChange', (event, attributes) => {
|
||||
plug.callPlugins('onNowPlayingItemDidChange', attributes);
|
||||
});
|
||||
|
||||
//
|
||||
electron.app.on('before-quit', () => {
|
||||
plug.callPlugins('onBeforeQuit');
|
||||
console.warn(`${electron.app.getName()} exited.`);
|
||||
});
|
||||
//
|
||||
// // @ts-ignore
|
||||
// // Widevine Stuff
|
||||
// electron.app.on('widevine-ready', (version, lastVersion) => {
|
||||
// if (null !== lastVersion) {
|
||||
// console.log('[Cider][Widevine] Widevine ' + version + ', upgraded from ' + lastVersion + ', is ready to be used!')
|
||||
// } else {
|
||||
// console.log('[Cider][Widevine] Widevine ' + version + ' is ready to be used!')
|
||||
// }
|
||||
// })
|
||||
|
||||
// // @ts-ignore
|
||||
// electron.app.on('widevine-update-pending', (currentVersion, pendingVersion) => {
|
||||
// console.log('[Cider][Widevine] Widevine ' + currentVersion + ' is ready to be upgraded to ' + pendingVersion + '!')
|
||||
// })
|
||||
|
||||
// // @ts-ignore
|
||||
// electron.app.on('widevine-error', (error) => {
|
||||
// console.log('[Cider][Widevine] Widevine installation encountered an error: ' + error)
|
||||
// electron.app.exit()
|
||||
// })
|
||||
|
||||
//
|
||||
//
|
||||
// app.on('open-url', (event, url) => {
|
||||
// event.preventDefault()
|
||||
// if (url.includes('ame://') || url.includes('itms://') || url.includes('itmss://') || url.includes('musics://') || url.includes('music://')) {
|
||||
// CiderBase.LinkHandler(url)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// app.on('second-instance', (_e, argv) => {
|
||||
// console.warn(`[InstanceHandler][SecondInstanceHandler] Second Instance Started with args: [${argv.join(', ')}]`)
|
||||
//
|
||||
// // Checks if first instance is authorized and if second instance has protocol args
|
||||
// argv.forEach((value) => {
|
||||
// if (value.includes('ame://') || value.includes('itms://') || value.includes('itmss://') || value.includes('musics://') || value.includes('music://')) {
|
||||
// console.warn(`[InstanceHandler][SecondInstanceHandler] Found Protocol!`)
|
||||
// CiderBase.LinkHandler(value);
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// if (argv.includes("--force-quit")) {
|
||||
// console.warn('[InstanceHandler][SecondInstanceHandler] Force Quit found. Quitting App.');
|
||||
// app.isQuiting = true
|
||||
// app.quit()
|
||||
// } else if (app.win && !app.cfg.get('advanced.allowMultipleInstances')) { // If a Second Instance has Been Started
|
||||
// console.warn('[InstanceHandler][SecondInstanceHandler] Showing window.');
|
||||
// app.win.show()
|
||||
// app.win.focus()
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// if (!app.requestSingleInstanceLock() && !app.cfg.get('advanced.allowMultipleInstances')) {
|
||||
// console.warn("[InstanceHandler] Existing Instance is Blocking Second Instance.");
|
||||
// app.quit();
|
||||
// app.isQuiting = true
|
||||
// }
|
|
@ -1,153 +0,0 @@
|
|||
const {app, Notification} = require('electron'),
|
||||
fs = require('fs'),
|
||||
{resolve} = require('path'),
|
||||
sessionPath = resolve(app.getPath('userData'), 'session.json'),
|
||||
apiCredentials = require('../../resources/lfmApiCredentials.json'),
|
||||
LastfmAPI = require('lastfmapi');
|
||||
|
||||
const lfm = {
|
||||
authenticateFromFile: function () {
|
||||
let sessionData = require(sessionPath)
|
||||
console.log("[LastFM][authenticateFromFile] Logging in with Session Info.")
|
||||
app.lastfm.setSessionCredentials(sessionData.name, sessionData.key)
|
||||
console.log("[LastFM][authenticateFromFile] Logged in.")
|
||||
},
|
||||
|
||||
authenticate: function () {
|
||||
if (app.cfg.get('lastfm.auth_token')) {
|
||||
app.cfg.set('lastfm.enabled', true);
|
||||
}
|
||||
|
||||
if (!app.cfg.get('lastfm.enabled') || !app.cfg.get('lastfm.auth_token')) {
|
||||
app.cfg.set('lastfm.enabled', false);
|
||||
return
|
||||
}
|
||||
|
||||
const lfmAPI = new LastfmAPI({
|
||||
'api_key': apiCredentials.key,
|
||||
'secret': apiCredentials.secret
|
||||
});
|
||||
|
||||
app.lastfm = Object.assign(lfmAPI, {cachedAttributes: false, cachedNowPlayingAttributes: false});
|
||||
|
||||
fs.stat(sessionPath, function (err) {
|
||||
if (err) {
|
||||
console.error("[LastFM][Session] Session file couldn't be opened or doesn't exist,", err)
|
||||
console.log("[LastFM][Auth] Beginning authentication from configuration")
|
||||
app.lastfm.authenticate(app.cfg.get('lastfm.auth_token'), function (err, session) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
console.log("[LastFM] Successfully obtained LastFM session info,", session); // {"name": "LASTFM_USERNAME", "key": "THE_USER_SESSION_KEY"}
|
||||
console.log("[LastFM] Saving session info to disk.")
|
||||
let tempData = JSON.stringify(session)
|
||||
fs.writeFile(sessionPath, tempData, (err) => {
|
||||
if (err)
|
||||
console.log("[LastFM][fs]", err)
|
||||
else {
|
||||
console.log("[LastFM][fs] File was written successfully.")
|
||||
lfm.authenticateFromFile()
|
||||
new Notification({
|
||||
title: app.getName(),
|
||||
body: "Successfully logged into LastFM using Authentication Key."
|
||||
}).show()
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
lfm.authenticateFromFile()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
scrobbleSong: async function (attributes) {
|
||||
await new Promise(resolve => setTimeout(resolve, Math.round(attributes.durationInMillis * (app.cfg.get('lastfm.scrobble_after') / 100))));
|
||||
const currentAttributes = app.media;
|
||||
|
||||
if (!app.lastfm || app.lastfm.cachedAttributes === attributes ) {
|
||||
return
|
||||
}
|
||||
|
||||
if (app.lastfm.cachedAttributes) {
|
||||
if (app.lastfm.cachedAttributes.playParams.id === attributes.playParams.id) return;
|
||||
}
|
||||
|
||||
if (currentAttributes.status && currentAttributes === attributes) {
|
||||
if (fs.existsSync(sessionPath)) {
|
||||
// Scrobble playing song.
|
||||
if (attributes.status === true) {
|
||||
app.lastfm.track.scrobble({
|
||||
'artist': lfm.filterArtistName(attributes.artistName),
|
||||
'track': attributes.name,
|
||||
'album': attributes.albumName,
|
||||
'albumArtist': this.filterArtistName(attributes.artistName),
|
||||
'timestamp': new Date().getTime() / 1000
|
||||
}, function (err, scrobbled) {
|
||||
if (err) {
|
||||
return console.error('[LastFM] An error occurred while scrobbling', err);
|
||||
}
|
||||
|
||||
console.log('[LastFM] Successfully scrobbled: ', scrobbled);
|
||||
});
|
||||
app.lastfm.cachedAttributes = attributes
|
||||
}
|
||||
} else {
|
||||
this.authenticate();
|
||||
}
|
||||
} else {
|
||||
return console.log('[LastFM] Did not add ', attributes.name , '—' , lfm.filterArtistName(attributes.artistName), 'because now playing a other song.');
|
||||
}
|
||||
},
|
||||
|
||||
filterArtistName: function (artist) {
|
||||
if (!app.cfg.get('lastfm.enabledRemoveFeaturingArtists')) return artist;
|
||||
|
||||
artist = artist.split(' ');
|
||||
if (artist.includes('&')) {
|
||||
artist.length = artist.indexOf('&');
|
||||
}
|
||||
if (artist.includes('and')) {
|
||||
artist.length = artist.indexOf('and');
|
||||
}
|
||||
artist = artist.join(' ');
|
||||
if (artist.includes(',')) {
|
||||
artist = artist.split(',')
|
||||
artist = artist[0]
|
||||
}
|
||||
return artist.charAt(0).toUpperCase() + artist.slice(1);
|
||||
},
|
||||
|
||||
updateNowPlayingSong: function (attributes) {
|
||||
if (!app.lastfm ||app.lastfm.cachedNowPlayingAttributes === attributes | !app.cfg.get('lastfm.NowPlaying')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (app.lastfm.cachedNowPlayingAttributes) {
|
||||
if (app.lastfm.cachedNowPlayingAttributes.playParams.id === attributes.playParams.id) return;
|
||||
}
|
||||
|
||||
if (fs.existsSync(sessionPath)) {
|
||||
// update Now Playing
|
||||
if (attributes.status === true) {
|
||||
app.lastfm.track.updateNowPlaying({
|
||||
'artist': lfm.filterArtistName(attributes.artistName),
|
||||
'track': attributes.name,
|
||||
'album': attributes.albumName,
|
||||
'albumArtist': this.filterArtistName(attributes.artistName)
|
||||
}, function (err, nowPlaying) {
|
||||
if (err) {
|
||||
return console.error('[LastFM] An error occurred while updating nowPlayingSong', err);
|
||||
}
|
||||
|
||||
console.log('[LastFM] Successfully updated nowPlayingSong', nowPlaying);
|
||||
});
|
||||
app.lastfm.cachedNowPlayingAttributes = attributes
|
||||
}
|
||||
|
||||
} else {
|
||||
this.authenticate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = lfm;
|
|
@ -1,149 +0,0 @@
|
|||
const { nativeImage } = require("electron");
|
||||
const path = require('path')
|
||||
|
||||
let mediaPlayer = null;
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* Connects to the MPRIS interface.
|
||||
* @param {Object} win - The BrowserWindow.
|
||||
*/
|
||||
connect: (win) => {
|
||||
if (process.platform !== "linux") return;
|
||||
|
||||
const Player = require('mpris-service');
|
||||
|
||||
mediaPlayer = Player({
|
||||
name: 'Cider',
|
||||
identity: 'Cider',
|
||||
supportedUriSchemes: [],
|
||||
supportedMimeTypes: [],
|
||||
supportedInterfaces: ['player']
|
||||
});
|
||||
mediaPlayer = Object.assign(mediaPlayer, { canQuit: true, canControl: true, canPause: true, canPlay: true, canGoNext: true })
|
||||
|
||||
|
||||
let pos_atr = {durationInMillis: 0};
|
||||
mediaPlayer.getPosition = function () {
|
||||
const durationInMicro = pos_atr.durationInMillis * 1000;
|
||||
const percentage = parseFloat("0") || 0;
|
||||
return durationInMicro * percentage;
|
||||
}
|
||||
|
||||
mediaPlayer.active = true
|
||||
|
||||
mediaPlayer.on('playpause', async () => {
|
||||
win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
|
||||
});
|
||||
|
||||
mediaPlayer.on('play', async () => {
|
||||
win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
|
||||
});
|
||||
|
||||
mediaPlayer.on('pause', async () => {
|
||||
win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
|
||||
});
|
||||
|
||||
mediaPlayer.on('next', async () => {
|
||||
win.webContents.executeJavaScript('MusicKitInterop.nextTrack()').catch(err => console.error(err))
|
||||
});
|
||||
|
||||
mediaPlayer.on('previous', async () => {
|
||||
win.webContents.executeJavaScript('MusicKitInterop.previousTrack()').catch(err => console.error(err))
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the MPRIS interface.
|
||||
* @param {Object} attributes - The attributes of the track.
|
||||
*/
|
||||
updateAttributes: (attributes) => {
|
||||
if (process.platform !== "linux") return;
|
||||
|
||||
const MetaData = {
|
||||
'mpris:trackid': mediaPlayer.objectPath(`track/${attributes.playParams.id.replace(/[.]+/g, "")}`),
|
||||
'mpris:length': attributes.durationInMillis * 1000, // In microseconds
|
||||
'mpris:artUrl': (attributes.artwork.url.replace('/{w}x{h}bb', '/512x512bb')).replace('/2000x2000bb', '/35x35bb'),
|
||||
'xesam:title': `${attributes.name}`,
|
||||
'xesam:album': `${attributes.albumName}`,
|
||||
'xesam:artist': [`${attributes.artistName}`,],
|
||||
'xesam:genre': attributes.genreNames
|
||||
}
|
||||
|
||||
if (mediaPlayer.metadata["mpris:trackid"] === MetaData["mpris:trackid"]) {
|
||||
return
|
||||
}
|
||||
|
||||
mediaPlayer.metadata = MetaData
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the playback state of the MPRIS interface.
|
||||
* @param {Object} attributes - The attributes of the track.
|
||||
*/
|
||||
updateState: (attributes) => {
|
||||
if (process.platform !== "linux") return;
|
||||
|
||||
function setPlaybackIfNeeded(status) {
|
||||
if (mediaPlayer.playbackStatus === status) {
|
||||
return
|
||||
}
|
||||
mediaPlayer.playbackStatus = status;
|
||||
}
|
||||
|
||||
switch (attributes.status) {
|
||||
case true: // Playing
|
||||
setPlaybackIfNeeded('Playing');
|
||||
break;
|
||||
case false: // Paused
|
||||
setPlaybackIfNeeded('Paused');
|
||||
break;
|
||||
default: // Stopped
|
||||
setPlaybackIfNeeded('Stopped');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
SetButtons: (win, attributes) => {
|
||||
if (process.platform === 'win32') { // Set the Windows Thumbnail Toolbar Buttons
|
||||
win.setThumbarButtons([
|
||||
{
|
||||
tooltip: 'Previous',
|
||||
icon: nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/backwardPng.png')),
|
||||
click() {
|
||||
console.log("Clicked the bc taskbar button!")
|
||||
win.webContents.executeJavaScript('MusicKitInterop.previousTrack()').catch(err => console.error(err))
|
||||
}
|
||||
},
|
||||
{
|
||||
tooltip: attributes.status ? 'Pause' : 'Play',
|
||||
//tooltip: 'Play',
|
||||
icon: attributes.status ? nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/pausePng.png')) : nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/playPng.png')),
|
||||
click() {
|
||||
console.log("Clicked the pl taskbar button!")
|
||||
win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
|
||||
}
|
||||
},
|
||||
{
|
||||
tooltip: 'Next',
|
||||
icon: nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/forwardPng.png')),
|
||||
click() {
|
||||
console.log("Clicked the fw taskbar button!")
|
||||
win.webContents.executeJavaScript('MusicKitInterop.nextTrack()').catch(err => console.error(err))
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes the MPRIS interface.
|
||||
*/
|
||||
clearActivity: () => {
|
||||
if (process.platform !== "linux") return;
|
||||
mediaPlayer.metadata = {'mpris:trackid': '/org/mpris/MediaPlayer2/TrackList/NoTrack'}
|
||||
mediaPlayer.playbackStatus = 'Stopped';
|
||||
},
|
||||
}
|
58
src/main/plugins/Extras/examplePlugin.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
let i = 1, k = 1;
|
||||
export default class ExamplePlugin {
|
||||
/**
|
||||
* Private variables for interaction in plugins
|
||||
*/
|
||||
private _win: any;
|
||||
private _app: any;
|
||||
|
||||
/**
|
||||
* Base Plugin Details (Eventually implemented into a GUI in settings)
|
||||
*/
|
||||
public name: string = 'examplePlugin';
|
||||
public description: string = 'Example plugin';
|
||||
public version: string = '1.0.0';
|
||||
public author: string = 'Example author';
|
||||
|
||||
/**
|
||||
* Runs on plugin load (Currently run on application start)
|
||||
*/
|
||||
constructor(app: any) {
|
||||
this._app = app;
|
||||
console.log('Example plugin loaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app ready
|
||||
*/
|
||||
onReady(win: any): void {
|
||||
this._win = win;
|
||||
console.log('Example plugin ready');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on app stop
|
||||
*/
|
||||
onBeforeQuit(): void {
|
||||
console.log('Example plugin stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on playback State Change
|
||||
* @param attributes Music Attributes (attributes.state = current state)
|
||||
*/
|
||||
onPlaybackStateDidChange(attributes: object): void {
|
||||
console.log('onPlaybackStateDidChange has been called ' + i +' times');
|
||||
i++
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on song change
|
||||
* @param attributes Music Attributes
|
||||
*/
|
||||
onNowPlayingItemDidChange(attributes: object): void {
|
||||
console.log('onNowPlayingDidChange has been called ' + k +' times');
|
||||
k++
|
||||
}
|
||||
|
||||
}
|
39
src/main/plugins/Extras/sendSongToTitlebar.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import * as electron from "electron";
|
||||
|
||||
export default class sendSongToTitlebar {
|
||||
/**
|
||||
* Base Plugin Details (Eventually implemented into a GUI in settings)
|
||||
*/
|
||||
public name: string = 'sendSongToTitlebar';
|
||||
public description: string = 'Sets the app\'s titlebar to the Song title';
|
||||
public version: string = '0.0.1';
|
||||
public author: string = 'Cider Collective (credit to 8times9 via #147)';
|
||||
/**
|
||||
* Runs on plugin load (Currently run on application start)
|
||||
*/
|
||||
private _win: any;
|
||||
private _app: any;
|
||||
constructor() {}
|
||||
/**
|
||||
* Runs on app ready
|
||||
*/
|
||||
onReady(win: any): void {
|
||||
this._win = win;
|
||||
}
|
||||
/**
|
||||
* Runs on app stop
|
||||
*/
|
||||
onBeforeQuit(): void {}
|
||||
/**
|
||||
* Runs on playback State Change
|
||||
* @param attributes Music Attributes (attributes.state = current state)
|
||||
*/
|
||||
onPlaybackStateDidChange(attributes: any): void {
|
||||
this._win.setTitle(`${(attributes != null && attributes.name != null && attributes.name.length > 0) ? (attributes.name + " - ") : ''}Cider`)
|
||||
}
|
||||
/**
|
||||
* Runs on song change
|
||||
* @param attributes Music Attributes
|
||||
*/
|
||||
onNowPlayingItemDidChange(attributes: object): void {}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
const electron = require('electron')
|
||||
|
||||
global.ipcRenderer = require('electron').ipcRenderer;
|
||||
console.log('Loaded Preload')
|
||||
|
||||
let cache = {playParams: {id: 0}, status: null, remainingTime: 0},
|
||||
|
@ -97,6 +96,5 @@ const MusicKitInterop = {
|
|||
|
||||
process.once('loaded', () => {
|
||||
console.log("Setting ipcRenderer")
|
||||
global.ipcRenderer = electron.ipcRenderer;
|
||||
global.MusicKitInterop = MusicKitInterop;
|
||||
});
|
5
src/renderer/.jsbeautifyrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"js": {
|
||||
"beautify.ignore": "src/renderer/index.js"
|
||||
}
|
||||
}
|
103
src/renderer/WSAPI_Interop.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
const wsapi = {
|
||||
cache: {playParams: {id: 0}, status: null, remainingTime: 0},
|
||||
playbackCache: {status: null, time: Date.now()},
|
||||
search(term, limit) {
|
||||
MusicKit.getInstance().api.search(term, {limit: limit, types: 'songs,artists,albums'}).then((results)=>{
|
||||
ipcRenderer.send('wsapi-returnSearch', JSON.stringify(results))
|
||||
})
|
||||
},
|
||||
searchLibrary(term, limit) {
|
||||
MusicKit.getInstance().api.library.search(term, {limit: limit, types: 'library-songs,library-artists,library-albums'}).then((results)=>{
|
||||
ipcRenderer.send('wsapi-returnSearchLibrary', JSON.stringify(results))
|
||||
})
|
||||
},
|
||||
getAttributes: function () {
|
||||
const mk = MusicKit.getInstance();
|
||||
const nowPlayingItem = mk.nowPlayingItem;
|
||||
const isPlayingExport = mk.isPlaying;
|
||||
const remainingTimeExport = mk.currentPlaybackTimeRemaining;
|
||||
const attributes = (nowPlayingItem != null ? nowPlayingItem.attributes : {});
|
||||
|
||||
attributes.status = isPlayingExport ? isPlayingExport : false;
|
||||
attributes.name = attributes.name ? attributes.name : 'No Title Found';
|
||||
attributes.artwork = attributes.artwork ? attributes.artwork : {url: ''};
|
||||
attributes.artwork.url = attributes.artwork.url ? attributes.artwork.url : '';
|
||||
attributes.playParams = attributes.playParams ? attributes.playParams : {id: 'no-id-found'};
|
||||
attributes.playParams.id = attributes.playParams.id ? attributes.playParams.id : 'no-id-found';
|
||||
attributes.albumName = attributes.albumName ? attributes.albumName : '';
|
||||
attributes.artistName = attributes.artistName ? attributes.artistName : '';
|
||||
attributes.genreNames = attributes.genreNames ? attributes.genreNames : [];
|
||||
attributes.remainingTime = remainingTimeExport ? (remainingTimeExport * 1000) : 0;
|
||||
attributes.durationInMillis = attributes.durationInMillis ? attributes.durationInMillis : 0;
|
||||
attributes.startTime = Date.now();
|
||||
attributes.endTime = attributes.endTime ? attributes.endTime : Date.now();
|
||||
attributes.volume = mk.volume;
|
||||
attributes.shuffleMode = mk.shuffleMode;
|
||||
attributes.repeatMode = mk.repeatMode;
|
||||
attributes.autoplayEnabled = mk.autoplayEnabled;
|
||||
return attributes
|
||||
},
|
||||
moveQueueItem(oldPosition, newPosition) {
|
||||
MusicKit.getInstance().queue._queueItems.splice(newPosition,0,MusicKit.getInstance().queue._queueItems.splice(oldPosition,1)[0])
|
||||
MusicKit.getInstance().queue._reindex()
|
||||
},
|
||||
setAutoplay(value) {
|
||||
MusicKit.getInstance().autoplayEnabled = value
|
||||
},
|
||||
returnDynamic(data, type) {
|
||||
ipcRenderer.send('wsapi-returnDynamic', JSON.stringify(data), type)
|
||||
},
|
||||
musickitApi(method, id, params) {
|
||||
MusicKit.getInstance().api[method](id, params).then((results)=>{
|
||||
ipcRenderer.send('wsapi-returnMusicKitApi', JSON.stringify(results), method)
|
||||
})
|
||||
},
|
||||
getPlaybackState () {
|
||||
ipcRenderer.send('wsapi-updatePlaybackState', MusicKitInterop.getAttributes());
|
||||
},
|
||||
getLyrics() {
|
||||
return []
|
||||
_lyrics.GetLyrics(1, false)
|
||||
},
|
||||
getQueue() {
|
||||
ipcRenderer.send('wsapi-returnQueue', JSON.stringify(MusicKit.getInstance().queue))
|
||||
},
|
||||
playNext(type, id) {
|
||||
var request = {}
|
||||
request[type] = id
|
||||
MusicKit.getInstance().playNext(request)
|
||||
},
|
||||
playLater(type, id) {
|
||||
var request = {}
|
||||
request[type] = id
|
||||
MusicKit.getInstance().playLater(request)
|
||||
},
|
||||
love() {
|
||||
|
||||
},
|
||||
playTrackById(id, kind = "song") {
|
||||
MusicKit.getInstance().setQueue({ [kind]: id }).then(function (queue) {
|
||||
MusicKit.getInstance().play()
|
||||
})
|
||||
},
|
||||
quickPlay(term) {
|
||||
// Quick play by song name
|
||||
MusicKit.getInstance().api.search(term, { limit: 2, types: 'songs' }).then(function (data) {
|
||||
MusicKit.getInstance().setQueue({ song: data["songs"][0]["id"] }).then(function (queue) {
|
||||
MusicKit.getInstance().play()
|
||||
})
|
||||
})
|
||||
},
|
||||
toggleShuffle() {
|
||||
MusicKit.getInstance().shuffleMode = MusicKit.getInstance().shuffleMode === 0 ? 1 : 0
|
||||
},
|
||||
toggleRepeat() {
|
||||
if(MusicKit.getInstance().repeatMode == 0) {
|
||||
MusicKit.getInstance().repeatMode = 2
|
||||
}else if(MusicKit.getInstance().repeatMode == 2){
|
||||
MusicKit.getInstance().repeatMode = 1
|
||||
}else{
|
||||
MusicKit.getInstance().repeatMode = 0
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@
|
|||
--navbarHeight: 48px;
|
||||
--selected: rgb(130 130 130 / 30%);
|
||||
--selected-click: rgb(80 80 80 / 30%);
|
||||
--hover: rgb(200 200 200 / 10%);
|
||||
--keyColor: #fa586a;
|
||||
--keyColor-rgb: 250, 88, 106;
|
||||
--keyColor-rollover: #ff8a9c;
|
||||
|
@ -254,6 +255,32 @@ input[type="text"], input[type="number"] {
|
|||
}
|
||||
}
|
||||
|
||||
.artworkMaterial {
|
||||
position: relative;
|
||||
height:100%;
|
||||
width:100%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
>img {
|
||||
position: absolute;
|
||||
width: 200%;
|
||||
opacity: 0.5;
|
||||
filter: brightness(200%) blur(180px) saturate(280%) contrast(2);
|
||||
}
|
||||
|
||||
>img:first-child {
|
||||
top:0;
|
||||
left:0;
|
||||
}
|
||||
|
||||
>img:last-child {
|
||||
bottom:0;
|
||||
right: 0;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[artwork-hidden] {
|
||||
transition: opacity .25s var(--appleEase);
|
||||
|
@ -1105,6 +1132,34 @@ input[type=range].web-slider::-webkit-slider-runnable-track {
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
filter: contrast(0.8);
|
||||
|
||||
.lcdMenu {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
appearance: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
&:active {
|
||||
background: var(--selected-click);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.svg-icon {
|
||||
--url: url('views/svg/more.svg')!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-chrome .app-chrome-item > .app-playback-controls .playback-info {
|
||||
|
@ -1896,6 +1951,36 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
|||
|
||||
/* Cider */
|
||||
|
||||
.more-btn-round {
|
||||
border-radius: 100%;
|
||||
background: rgba(100, 100, 100, 0.5);
|
||||
box-shadow: var(--ciderShadow-Generic);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0px;
|
||||
cursor: pointer;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: brightness(75%);
|
||||
transform: scale(0.98);
|
||||
transition: transform 0s var(--appleEase), box-shadow 0.2s var(--appleEase);
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 100%;
|
||||
background: #eee;
|
||||
--url: url("./views/svg/more.svg");
|
||||
}
|
||||
}
|
||||
|
||||
.about-page {
|
||||
.teamBtn {
|
||||
display: flex;
|
||||
|
@ -1951,6 +2036,14 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
|||
|
||||
&.md-btn-block {
|
||||
display: block;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
&.md-btn-glyph {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.md-btn-primary {
|
||||
|
@ -2398,115 +2491,171 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
|||
.playlist-page {
|
||||
--bgColor: transparent;
|
||||
padding: 0px;
|
||||
background: linear-gradient(180deg, var(--bgColor) 32px, var(--bgColor) 59px, transparent 60px, transparent 100%);
|
||||
//background: linear-gradient(180deg, var(--bgColor) 32px, var(--bgColor) 18px, transparent 60px, transparent 100%);
|
||||
top: 0;
|
||||
padding-top: var(--navigationBarHeight);
|
||||
|
||||
.playlist-body {
|
||||
padding: var(--contentInnerPadding);
|
||||
padding: 0px var(--contentInnerPadding) 0px var(--contentInnerPadding);
|
||||
}
|
||||
|
||||
.floating-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-bottom: 1px solid rgba(200, 200, 200, 0.05);
|
||||
z-index: 6;
|
||||
padding: 0px 1em;
|
||||
backdrop-filter: blur(32px);
|
||||
background: rgba(24, 24, 24, 0.15);
|
||||
top: var(--navigationBarHeight);
|
||||
transition: opacity 0.1s var(--appleEase);
|
||||
}
|
||||
|
||||
.playlist-display {
|
||||
padding: var(--contentInnerPadding);
|
||||
min-height: 300px;
|
||||
position: relative;
|
||||
|
||||
.playlist-info {
|
||||
flex-shrink: unset;
|
||||
.artworkContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-mask-image: radial-gradient(at top left, black, transparent 70%), radial-gradient(at top right, black, transparent 70%), linear-gradient(180deg, rgb(200 200 200), transparent 98%);
|
||||
opacity: .7;
|
||||
animation: playlistArtworkFadeIn 1s var(--appleEase);
|
||||
|
||||
.artworkMaterial>img {
|
||||
filter: brightness(100%) blur(80px) saturate(100%) contrast(1);
|
||||
object-position: center;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.playlistInfo {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-end;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.playlist-name {
|
||||
font-weight: 700;
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: unset;
|
||||
>.row {
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.nameEdit {
|
||||
font-weight: 700;
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 6px;
|
||||
.playlist-info {
|
||||
flex-shrink: unset;
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
.playlist-artist {
|
||||
font-size: 20px;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: unset;
|
||||
}
|
||||
|
||||
.playlist-desc {
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
flex-shrink: unset;
|
||||
margin-right: 5px;
|
||||
max-height: 100px;
|
||||
position: relative;
|
||||
|
||||
.content {
|
||||
height: 100px;
|
||||
-webkit-mask-image: -webkit-gradient(linear, left 50%, left 90%, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
|
||||
.playlist-name {
|
||||
font-weight: 700;
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: unset;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 0 5px;
|
||||
font-size: 14px;
|
||||
color: var(--keyColor);
|
||||
background-color: transparent;
|
||||
.nameEdit {
|
||||
font-weight: 700;
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: unset;
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-desc-expanded {
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
.playlist-artist {
|
||||
font-size: 20px;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: unset;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 0 5px;
|
||||
.playlist-desc {
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
color: var(--keyColor);
|
||||
background-color: transparent;
|
||||
border: 0px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: unset;
|
||||
margin-right: 5px;
|
||||
max-height: 100px;
|
||||
position: relative;
|
||||
|
||||
.content {
|
||||
height: 100px;
|
||||
-webkit-mask-image: -webkit-gradient(linear, left 50%, left 90%, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 0 5px;
|
||||
font-size: 14px;
|
||||
color: var(--keyColor);
|
||||
background-color: transparent;
|
||||
border: 0px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-desc-expanded {
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
|
||||
.more-btn {
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 0 5px;
|
||||
font-size: 14px;
|
||||
color: var(--keyColor);
|
||||
background-color: transparent;
|
||||
border: 0px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.friends-info {
|
||||
|
@ -2539,26 +2688,6 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
|||
}
|
||||
}
|
||||
|
||||
.playlist-more {
|
||||
border-radius: 100%;
|
||||
background: var(--keyColor);
|
||||
box-shadow: var(--ciderShadow-Generic);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
float: right;
|
||||
border: 0px;
|
||||
cursor: pointer;
|
||||
z-index: 5;
|
||||
|
||||
&:hover {
|
||||
background: var(--keyColor-rollover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--keyColor-pressed);
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-time {
|
||||
font-size: 0.9em;
|
||||
margin: 6px;
|
||||
|
@ -2566,6 +2695,14 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes playlistArtworkFadeIn {
|
||||
0%{
|
||||
opacity: 0;
|
||||
}
|
||||
100%{
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
// Collection Page
|
||||
.collection-page {
|
||||
padding-bottom: 128px;
|
||||
|
@ -2608,8 +2745,21 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
|||
padding: 0px;
|
||||
top: 0;
|
||||
|
||||
.floating-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-bottom: 1px solid rgba(200, 200, 200, 0.05);
|
||||
z-index: 6;
|
||||
padding: 0px 1em;
|
||||
backdrop-filter: blur(32px);
|
||||
background: rgba(24, 24, 24, 0.15);
|
||||
top: var(--navigationBarHeight);
|
||||
transition: opacity 0.1s var(--appleEase);
|
||||
}
|
||||
|
||||
.artist-header {
|
||||
background: linear-gradient(45deg, var(--keyColor), #0e0e0e);
|
||||
//background: linear-gradient(45deg, var(--keyColor), #0e0e0e);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -2617,26 +2767,36 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
|||
min-height: 400px;
|
||||
position: relative;
|
||||
|
||||
.artist-more {
|
||||
border-radius: 100%;
|
||||
background: var(--keyColor);
|
||||
box-shadow: var(--ciderShadow-Generic);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
.header-content {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.artworkContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-mask-image: radial-gradient(at top left, black, transparent 70%), radial-gradient(at top right, black, transparent 70%), linear-gradient(180deg, rgb(200 200 200), transparent 98%);
|
||||
opacity: .7;
|
||||
animation: playlistArtworkFadeIn 1s var(--appleEase);
|
||||
|
||||
.artworkMaterial>img {
|
||||
filter: brightness(100%) blur(80px) saturate(100%) contrast(1);
|
||||
object-position: center;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.more-btn-round {
|
||||
position: absolute;
|
||||
bottom: 26px;
|
||||
right: 32px;
|
||||
border: 0px;
|
||||
cursor: pointer;
|
||||
z-index: 5;
|
||||
|
||||
&:hover {
|
||||
background: var(--keyColor-rollover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--keyColor-pressed);
|
||||
}
|
||||
}
|
||||
|
||||
.animated {
|
||||
|
@ -2703,28 +2863,31 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
|||
}
|
||||
}
|
||||
|
||||
.artist-play {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--keyColor);
|
||||
border-radius: 100%;
|
||||
box-shadow: var(--mediaItemShadow);
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
|
||||
&:hover {
|
||||
background: var(--keyColor-rollover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--keyColor-pressed);
|
||||
}
|
||||
}
|
||||
.artist-title {
|
||||
|
||||
.artist-play {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--keyColor);
|
||||
border-radius: 100%;
|
||||
margin: 14px;
|
||||
box-shadow: var(--mediaItemShadow);
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
transform: translateY(3px);
|
||||
|
||||
&:hover {
|
||||
background: var(--keyColor-rollover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--keyColor-pressed);
|
||||
}
|
||||
margin: 14px;
|
||||
}
|
||||
|
||||
&.artist-animation-on {
|
||||
|
@ -2742,7 +2905,8 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
|||
}
|
||||
|
||||
.artist-body {
|
||||
padding: var(--contentInnerPadding);
|
||||
padding: 0px var(--contentInnerPadding) 0px var(--contentInnerPadding);
|
||||
margin-top: -48px;
|
||||
}
|
||||
|
||||
.showmoreless {
|
||||
|
|
37
src/renderer/views/components/artwork-material.ejs
Normal file
|
@ -0,0 +1,37 @@
|
|||
<script type="text/x-template" id="artwork-material">
|
||||
<div class="artworkMaterial">
|
||||
<img :src="src" v-for="image in images"/>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
Vue.component('artwork-material', {
|
||||
template: '#artwork-material',
|
||||
data: function () {
|
||||
return {
|
||||
src: ""
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.src = app.getMediaItemArtwork(this.url, this.size)
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: '32'
|
||||
},
|
||||
images: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: '2'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -70,14 +70,14 @@
|
|||
},
|
||||
async select(e) {
|
||||
let u = this.item
|
||||
let u1 = await app.mk.api.library.artistRelationship(u.id,"albums",
|
||||
let u1 = await app.mk.api.v3.music(`/v1/me/library/artists/${u.id}/albums`,
|
||||
{platform: "web",
|
||||
"include[library-albums]": "artists,tracks",
|
||||
"include[library-artists]": "catalog",
|
||||
"fields[artists]": "url",
|
||||
"includeOnly": "catalog,artists"}
|
||||
)
|
||||
app.showCollection({data : Object.assign({},u1)}, u.attributes.name?? '', '');
|
||||
app.showCollection({data : Object.assign({},u1.data.data)}, u.attributes.name?? '', '');
|
||||
},
|
||||
getArtwork(){
|
||||
let u = ""
|
||||
|
|
0
src/renderer/views/components/mediaitem-info.ejs
Normal file
|
@ -502,22 +502,21 @@
|
|||
app.mk.setQueue({[truekind]: [item.attributes.playParams.id ?? item.id]}).then(function () {
|
||||
app.mk.play().then(function (){
|
||||
var playlistId = id
|
||||
function getPlaylist(id, params, isLibrary){
|
||||
function getPlaylist(id, isLibrary){
|
||||
if (isLibrary){
|
||||
return app.mk.api.library.playlist(id, params)
|
||||
} else { return app.mk.api.playlist(id, params)}
|
||||
return this.app.mk.api.v3.music(`/v1/me/library/playlists/${id}`)
|
||||
} else { return this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/playlists/${id}`)}
|
||||
}
|
||||
try {
|
||||
|
||||
getPlaylist(id, params, isLibrary).then(res => {
|
||||
getPlaylist(id, isLibrary).then(res => {
|
||||
//let query = res.relationships.tracks.data.map(item => new MusicKit.MediaItem(item));
|
||||
//if (app.mk.shuffleMode == 1){shuffleArray(query); }
|
||||
// console.log(query)
|
||||
// app.mk.queue.append(query)
|
||||
if (!res.relationships.tracks.next) {
|
||||
if (!res.data.relationships.tracks.next) {
|
||||
return
|
||||
} else {
|
||||
getPlaylistTracks(res.relationships.tracks.next)
|
||||
getPlaylistTracks(res.data.relationships.tracks.next)
|
||||
}
|
||||
|
||||
function getPlaylistTracks(next) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script type="text/x-template" id="mediaitem-scroller-horizontal">
|
||||
<template>
|
||||
<div class="cd-hmedia-scroller" :class="kind">
|
||||
<slot></slot>
|
||||
<mediaitem-square :kind="kind" :item="item"
|
||||
v-for="item in items"></mediaitem-square>
|
||||
</div>
|
||||
|
@ -13,7 +14,7 @@
|
|||
props: {
|
||||
'items': {
|
||||
type: Array,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
'kind': {
|
||||
type: String,
|
||||
|
|
|
@ -122,7 +122,11 @@
|
|||
}
|
||||
let kind = this.item.attributes.playParams.kind ?? this.item.type ?? '';
|
||||
var truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
|
||||
app.mk.api.library.remove({[truekind]: id})
|
||||
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
|
||||
{
|
||||
fetchOptions: {
|
||||
method: "DELETE"
|
||||
}})
|
||||
this.addedToLibrary = true
|
||||
},
|
||||
async contextMenu(event) {
|
||||
|
|
|
@ -96,6 +96,7 @@
|
|||
if (this.item.type && !this.item.type.includes("library")) {
|
||||
var params = {"fields[playlists]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library", "extend": this.revisedRandId()}
|
||||
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
|
||||
res = res.data.data[0]
|
||||
this.addedToLibrary = (res && res.attributes && res.attributes.inLibrary) ? res.attributes.inLibrary : false
|
||||
} else {
|
||||
this.addedToLibrary = true
|
||||
|
@ -105,12 +106,17 @@
|
|||
var params = {"fields[playlists]": "inLibrary","fields[songs]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library", "extend": this.revisedRandId()}
|
||||
var id = this.item.id ?? this.item.attributes.playParams.id
|
||||
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
|
||||
res = res.data.data[0]
|
||||
if (res && res.relationships && res.relationships.library && res.relationships.library.data && res.relationships.library.data.length > 0) {
|
||||
id = res.relationships.library.data[0].id
|
||||
}
|
||||
let kind = this.item.attributes.playParams.kind ?? this.item.type ?? '';
|
||||
var truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
|
||||
app.mk.api.library.remove({[truekind]: id})
|
||||
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
|
||||
{
|
||||
fetchOptions: {
|
||||
method: "DELETE"
|
||||
}})
|
||||
this.addedToLibrary = true
|
||||
},
|
||||
subtitleSearchNavigate(item) {
|
||||
|
|
|
@ -145,8 +145,8 @@
|
|||
let friends = this.badges[id]
|
||||
if (friends) {
|
||||
friends.forEach(function (friend) {
|
||||
self.app.mk.api.socialProfile(friend).then(data => {
|
||||
self.itemBadges.push(data)
|
||||
self.app.mk.api.v3.music(`/v1/social/${app.mk.storefrontId}/social-profiles/${friend}`).then(data => {
|
||||
self.itemBadges.push(data.data.data[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -164,6 +164,7 @@
|
|||
"extend": this.revisedRandId()
|
||||
}
|
||||
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
|
||||
res = res.data.data[0]
|
||||
this.addedToLibrary = (res && res.attributes && res.attributes.inLibrary) ? res.attributes.inLibrary : false
|
||||
} else {
|
||||
this.addedToLibrary = true
|
||||
|
@ -179,12 +180,17 @@
|
|||
}
|
||||
var id = this.item.id ?? this.item.attributes.playParams.id
|
||||
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
|
||||
res= res.data.data[0]
|
||||
if (res && res.relationships && res.relationships.library && res.relationships.library.data && res.relationships.library.data.length > 0) {
|
||||
id = res.relationships.library.data[0].id
|
||||
}
|
||||
let kind = this.item.attributes.playParams.kind ?? this.item.type ?? '';
|
||||
var truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
|
||||
app.mk.api.library.remove({[truekind]: id})
|
||||
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
|
||||
{
|
||||
fetchOptions: {
|
||||
method: "DELETE"
|
||||
}})
|
||||
this.addedToLibrary = true
|
||||
},
|
||||
uuidv4() {
|
||||
|
|
|
@ -181,7 +181,9 @@
|
|||
this.children = []
|
||||
this.getChildren()
|
||||
this.toggleFolder()
|
||||
this.$root.mk.api.library.playlistFolderChildren(item.id).then(children => {
|
||||
|
||||
this.$root.mk.api.v3.music(`v1/me/library/playlist-folders/${item.id}/children`).then(data => {
|
||||
let children = data.data.data;
|
||||
children.forEach(child => {
|
||||
if(!self.$root.playlists.listing.find(listing => listing.id == child.id)) {
|
||||
child.parent = self.item.id
|
||||
|
|
|
@ -115,11 +115,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<template v-if="mk.nowPlayingItem['attributes']['playParams']">
|
||||
<div class="actions"
|
||||
v-if="isInLibrary(mk.nowPlayingItem['attributes']['playParams'])">
|
||||
❤️
|
||||
<div class="actions">
|
||||
<button class="lcdMenu" @click="nowPlayingContextMenu">
|
||||
<div class="svg-icon"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="actions" v-else>🖤</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
@ -661,6 +661,8 @@
|
|||
</button>
|
||||
</script>
|
||||
|
||||
<!-- Artwork Material -->
|
||||
<%- include('components/artwork-material') %>
|
||||
<!-- Menu Panel -->
|
||||
<%- include('components/menu-panel') %>
|
||||
<!-- Playlist Listing -->
|
||||
|
@ -712,5 +714,6 @@
|
|||
<script src="index.js?v=1"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/resonance-audio/build/resonance-audio.min.js"></script>
|
||||
<script src="/audio/audio.js?v=1"></script>
|
||||
<script src="/WSAPI_Interop.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,5 +1,30 @@
|
|||
<script type="text/x-template" id="cider-artist-feed">
|
||||
<div class="content-inner">
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row nopadding">
|
||||
<div class="col nopadding">
|
||||
<h3>Followed Artists</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="well">
|
||||
<mediaitem-scroller-horizontal>
|
||||
<div v-for="artist in artists" style="margin: 6px;">
|
||||
<mediaitem-square :item="artist" kind="small"></mediaitem-square>
|
||||
<button @click="unfollow(artist.id)" class="md-btn md-btn-glyph" style="display:flex;">
|
||||
<div class="sidebar-icon">
|
||||
<div class="svg-icon" :style="{'--url': 'url(./assets/feather/x-circle.svg)'}"></div>
|
||||
</div> Unfollow
|
||||
</button>
|
||||
</div>
|
||||
</mediaitem-scroller-horizontal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
|
@ -30,6 +55,7 @@
|
|||
app: this.$root,
|
||||
followedArtists: this.$root.cfg.home.followedArtists,
|
||||
artistFeed: [],
|
||||
artists: []
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
|
@ -37,19 +63,26 @@
|
|||
await this.getArtistFeed()
|
||||
},
|
||||
methods: {
|
||||
unfollow(id) {
|
||||
let index = this.followedArtists.indexOf(id)
|
||||
if (index > -1) {
|
||||
this.followedArtists.splice(index, 1)
|
||||
}
|
||||
let artist = this.artists.find(a => a.id == id)
|
||||
let index2 = this.artists.indexOf(artist)
|
||||
if (index2 > -1) {
|
||||
this.artists.splice(index2, 1)
|
||||
}
|
||||
this.getArtistFeed()
|
||||
},
|
||||
async getArtistFeed() {
|
||||
let artists = this.followedArtists
|
||||
let self = this
|
||||
this.app.mk.api.artists(artists, {
|
||||
"views": "featured-release,full-albums,appears-on-albums,featured-albums,featured-on-albums,singles,compilation-albums,live-albums,latest-release,top-music-videos,similar-artists,top-songs,playlists,more-to-hear,more-to-see",
|
||||
"extend": "artistBio,bornOrFormed,editorialArtwork,editorialVideo,isGroup,origin,hero",
|
||||
"extend[playlists]": "trackCount",
|
||||
"include[songs]": "albums",
|
||||
"fields[albums]": "artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount",
|
||||
"limit[artists:top-songs]": 20,
|
||||
"art[url]": "f"
|
||||
}, {includeResponseMeta: !0}).then(artistData => {
|
||||
artistData.data.forEach(item => {
|
||||
this.artists = []
|
||||
this.artistFeed = []
|
||||
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists?ids=${artists.toString()}&views=latest-release&include[songs]=albums&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount&limit[artists:top-songs]=2&art[url]=f`).then(artistData => {
|
||||
artistData.data.data.forEach(item => {
|
||||
self.artists.push(item)
|
||||
if (item.views["latest-release"].data.length != 0) {
|
||||
self.artistFeed.push(item.views["latest-release"].data[0])
|
||||
}
|
||||
|
|
|
@ -1,42 +1,59 @@
|
|||
<script type="text/x-template" id="cider-artist">
|
||||
<div class="content-inner artist-page">
|
||||
<div class="artist-header" :style="getArtistPalette(data)" :key="data.id">
|
||||
<div class="artist-header" :key="data.id" v-observe-visibility="{callback: isHeaderVisible}">
|
||||
<animatedartwork-view
|
||||
:priority="true"
|
||||
v-if="data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9)"
|
||||
:video="data.attributes.editorialVideo.motionArtistWide16x9.video ?? (data.attributes.editorialVideo.motionArtistFullscreen16x9.video ?? '')">
|
||||
</animatedartwork-view>
|
||||
<div class="row">
|
||||
<div class="col-sm" style="width: auto;">
|
||||
<div class="artist-image" v-if="!(data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9))">
|
||||
<mediaitem-artwork
|
||||
shadow="large"
|
||||
:url="data.attributes.artwork ? data.attributes.artwork.url : ''"
|
||||
size="190" type="artists"></mediaitem-artwork>
|
||||
<button class="overlay-play" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
|
||||
<div class="header-content">
|
||||
<div class="row">
|
||||
<div class="col-sm" style="width: auto;">
|
||||
<div class="artist-image" v-if="!(data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9))">
|
||||
<mediaitem-artwork
|
||||
shadow="large"
|
||||
:url="data.attributes.artwork ? data.attributes.artwork.url : ''"
|
||||
size="190" type="artists"></mediaitem-artwork>
|
||||
<button class="overlay-play" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
|
||||
app.mk.play()
|
||||
})">
|
||||
<%- include("../svg/play.svg") %>
|
||||
</button>
|
||||
<%- include("../svg/play.svg") %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center artist-title"
|
||||
:class="{'artist-animation-on': (data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9)) }"
|
||||
>
|
||||
<button class="artist-play" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
|
||||
<div class="col flex-center artist-title"
|
||||
:class="{'artist-animation-on': (data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9)) }"
|
||||
>
|
||||
<button class="artist-play" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
|
||||
app.mk.play()
|
||||
})"><%- include("../svg/play.svg") %></button>
|
||||
<h1>{{ data.attributes.name }}</h1>
|
||||
<h1>{{ data.attributes.name }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<button class="more-btn-round" @click="artistMenu">
|
||||
<div class="svg-icon"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="artworkContainer" v-if="!(data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9))">
|
||||
<artwork-material :url="data.attributes.artwork.url" size="190" images="1"></artwork-material>
|
||||
</div>
|
||||
</div>
|
||||
<div class="floating-header" :style="{opacity: (headerVisible ? 0 : 1),'pointer-events': (headerVisible ? 'none' : '')}">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<button class="artist-play" style="display:block;" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
|
||||
app.mk.play()
|
||||
})"><%- include("../svg/play.svg") %></button>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ data.attributes.name }}</h3>
|
||||
</div>
|
||||
<div class="col-auto flex-center">
|
||||
<button class="more-btn-round" @click="artistMenu">
|
||||
<div class="svg-icon"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="artist-more" @click="artistMenu">
|
||||
<div style=" margin-top: -1px;
|
||||
margin-left: -6px;
|
||||
width: 36px;
|
||||
height: 36px;">
|
||||
<%- include("../svg/more.svg") %>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="artist-body">
|
||||
<div class="row well">
|
||||
|
@ -133,10 +150,14 @@
|
|||
data: function () {
|
||||
return {
|
||||
topSongsExpanded: false,
|
||||
app: this.$root
|
||||
app: this.$root,
|
||||
headerVisible: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isHeaderVisible(visible) {
|
||||
this.headerVisible = visible
|
||||
},
|
||||
artistMenu (event) {
|
||||
let self = this
|
||||
let followAction = "follow"
|
||||
|
|
|
@ -7,81 +7,118 @@
|
|||
</div>
|
||||
</template>
|
||||
<template v-if="app.playlists.loadingState == 1">
|
||||
<div class="playlist-display row"
|
||||
<div class="playlist-display"
|
||||
:style="{
|
||||
background: (data.attributes.artwork != null && data.attributes.artwork['bgColor'] != null) ? ('#' + data.attributes.artwork.bgColor) : '',
|
||||
color: (data.attributes.artwork != null && data.attributes.artwork['textColor1'] != null) ? ('#' + data.attributes.artwork.textColor1) : ''
|
||||
'--bgColor': (data.attributes.artwork != null && data.attributes.artwork['bgColor'] != null) ? ('#' + data.attributes.artwork.bgColor) : '',
|
||||
'--textColor': (data.attributes.artwork != null && data.attributes.artwork['textColor1'] != null) ? ('#' + data.attributes.artwork.textColor1) : ''
|
||||
}">
|
||||
<div class="col-auto flex-center">
|
||||
<div style="width: 260px;height:260px;">
|
||||
<mediaitem-artwork
|
||||
:video-priority="true"
|
||||
:url="(data.attributes != null && data.attributes.artwork != null) ? data.attributes.artwork.url : ((data.relationships != null && data.relationships.tracks.data.length > 0 && data.relationships.tracks.data[0].attributes != null) ? ((data.relationships.tracks.data[0].attributes.artwork != null)? data.relationships.tracks.data[0].attributes.artwork.url : ''):'')"
|
||||
:video="(data.attributes != null && data.attributes.editorialVideo != null) ? (data.attributes.editorialVideo.motionDetailSquare ? data.attributes.editorialVideo.motionDetailSquare.video : (data.attributes.editorialVideo.motionSquareVideo1x1 ? data.attributes.editorialVideo.motionSquareVideo1x1.video : '')) : '' "
|
||||
size="260"
|
||||
></mediaitem-artwork>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col playlist-info">
|
||||
<template v-if="!editorialNotesExpanded">
|
||||
<div>
|
||||
<div class="playlist-name" @click="editPlaylistName()" v-show="!nameEditing">
|
||||
{{data.attributes ? (data.attributes.name ??
|
||||
(data.attributes.title ?? '') ?? '') : ''}}
|
||||
<div class="playlistInfo">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div style="width: 260px;height:260px;">
|
||||
<mediaitem-artwork
|
||||
shadow="large"
|
||||
:video-priority="true"
|
||||
:url="(data.attributes != null && data.attributes.artwork != null) ? data.attributes.artwork.url : ((data.relationships != null && data.relationships.tracks.data.length > 0 && data.relationships.tracks.data[0].attributes != null) ? ((data.relationships.tracks.data[0].attributes.artwork != null)? data.relationships.tracks.data[0].attributes.artwork.url : ''):'')"
|
||||
:video="(data.attributes != null && data.attributes.editorialVideo != null) ? (data.attributes.editorialVideo.motionDetailSquare ? data.attributes.editorialVideo.motionDetailSquare.video : (data.attributes.editorialVideo.motionSquareVideo1x1 ? data.attributes.editorialVideo.motionSquareVideo1x1.video : '')) : '' "
|
||||
size="260"
|
||||
></mediaitem-artwork>
|
||||
</div>
|
||||
<div class="playlist-name" v-show="nameEditing"><input type="text" spellcheck="false"
|
||||
class="nameEdit"
|
||||
v-model="data.attributes.name"
|
||||
@blur="editPlaylist"
|
||||
@change="editPlaylist"
|
||||
@keydown.enter="editPlaylist"/></div>
|
||||
<div class="playlist-artist item-navigate"
|
||||
v-if="getArtistName(data) != ''"
|
||||
@click="data.attributes && data.attributes.artistName ? app.searchAndNavigate(data,'artist') : ''">
|
||||
{{getArtistName(data)}}
|
||||
</div>
|
||||
<div class="playlist-desc" v-if="data.attributes.description && (data.attributes.description.standard || data.attributes.description.short)">
|
||||
<div v-if="data.attributes.description.short" class="content" v-html="data.attributes.description.short"></div>
|
||||
<div v-else-if="data.attributes.description.standard" class="content" v-html="data.attributes.description.standard"></div>
|
||||
<button v-if="data.attributes.description.short" class="more-btn"
|
||||
@click="editorialNotesExpanded = !editorialNotesExpanded">
|
||||
More
|
||||
</div>
|
||||
<div class="col playlist-info">
|
||||
<template v-if="!editorialNotesExpanded">
|
||||
<div>
|
||||
<div class="playlist-name" @click="editPlaylistName()" v-show="!nameEditing">
|
||||
{{data.attributes ? (data.attributes.name ??
|
||||
(data.attributes.title ?? '') ?? '') : ''}}
|
||||
</div>
|
||||
<div class="playlist-name" v-show="nameEditing"><input type="text" spellcheck="false"
|
||||
class="nameEdit"
|
||||
v-model="data.attributes.name"
|
||||
@blur="editPlaylist"
|
||||
@change="editPlaylist"
|
||||
@keydown.enter="editPlaylist"/></div>
|
||||
<div class="playlist-artist item-navigate"
|
||||
v-if="getArtistName(data) != ''"
|
||||
@click="data.attributes && data.attributes.artistName ? app.searchAndNavigate(data,'artist') : ''">
|
||||
{{getArtistName(data)}}
|
||||
</div>
|
||||
<div class="playlist-desc" v-if="data.attributes.description && (data.attributes.description.standard || data.attributes.description.short)">
|
||||
<div v-if="data.attributes.description.short" class="content" v-html="data.attributes.description.short"></div>
|
||||
<div v-else-if="data.attributes.description.standard" class="content" v-html="data.attributes.description.standard"></div>
|
||||
<button v-if="data.attributes.description.short" class="more-btn"
|
||||
@click="editorialNotesExpanded = !editorialNotesExpanded">
|
||||
More
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="editorialNotesExpanded">
|
||||
<div class="playlist-desc-expanded">
|
||||
<div class="content"
|
||||
v-html="((data.attributes.editorialNotes) ? (data.attributes.editorialNotes.standard ?? (data.attributes.editorialNotes.short ?? '') ) : (data.attributes.description ? (data.attributes.description.standard ?? (data.attributes.description.short ?? '')) : ''))"></div>
|
||||
<button class="more-btn" @click="editorialNotesExpanded = !editorialNotesExpanded">Less
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="playlist-controls" v-observe-visibility="{callback: isHeaderVisible}">
|
||||
<button class="md-btn" style="min-width: 120px;"
|
||||
@click="app.mk.shuffleMode = 0; play()">
|
||||
Play
|
||||
</button>
|
||||
<button class="md-btn" style="min-width: 120px;"
|
||||
@click="app.mk.shuffleMode = 1;play()">
|
||||
Shuffle
|
||||
</button>
|
||||
<button class="md-btn" style="min-width: 120px;" v-if="inLibrary!=null && confirm!=true"
|
||||
@click="confirmButton()">
|
||||
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
|
||||
</button>
|
||||
<button class="md-btn" style="min-width: 120px;" v-if="confirm==true"
|
||||
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
|
||||
Confirm?
|
||||
</button>
|
||||
<button class="more-btn-round" style="float:right;" @click="menu">
|
||||
<div class="svg-icon"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="editorialNotesExpanded">
|
||||
<div class="playlist-desc-expanded">
|
||||
<div class="content"
|
||||
v-html="((data.attributes.editorialNotes) ? (data.attributes.editorialNotes.standard ?? (data.attributes.editorialNotes.short ?? '') ) : (data.attributes.description ? (data.attributes.description.standard ?? (data.attributes.description.short ?? '')) : ''))"></div>
|
||||
<button class="more-btn" @click="editorialNotesExpanded = !editorialNotesExpanded">Less
|
||||
</div>
|
||||
</div>
|
||||
<div class="artworkContainer" v-if="data.attributes.artwork != null">
|
||||
<artwork-material :url="data.attributes.artwork.url" size="260" images="1"></artwork-material>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="floating-header" :style="{opacity: (headerVisible ? 0 : 1),'pointer-events': (headerVisible ? 'none' : '')}">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{data.attributes ? (data.attributes.name ??
|
||||
(data.attributes.title ?? '') ?? '') : ''}}</h3>
|
||||
</div>
|
||||
<div class="col-auto flex-center">
|
||||
<div>
|
||||
<button class="md-btn" style="min-width: 120px;"
|
||||
@click="app.mk.shuffleMode = 0; play()">
|
||||
Play
|
||||
</button>
|
||||
<button class="md-btn" style="min-width: 120px;"
|
||||
@click="app.mk.shuffleMode = 1;play()">
|
||||
Shuffle
|
||||
</button>
|
||||
<button class="md-btn" style="min-width: 120px;" v-if="inLibrary!=null && confirm!=true"
|
||||
@click="confirmButton()">
|
||||
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
|
||||
</button>
|
||||
<button class="md-btn" style="min-width: 120px;" v-if="confirm==true"
|
||||
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
|
||||
Confirm?
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="playlist-controls">
|
||||
<button class="md-btn" style="min-width: 120px;"
|
||||
@click="app.mk.shuffleMode = 0; play()">
|
||||
Play
|
||||
</button>
|
||||
<button class="md-btn" style="min-width: 120px;"
|
||||
@click="app.mk.shuffleMode = 1;play()">
|
||||
Shuffle
|
||||
</button>
|
||||
<button class="md-btn" style="min-width: 120px;" v-if="inLibrary!=null && confirm!=true"
|
||||
@click="confirmButton()">
|
||||
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
|
||||
</button>
|
||||
<button class="md-btn" style="min-width: 120px;" v-if="confirm==true"
|
||||
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
|
||||
Confirm?
|
||||
</button>
|
||||
<button class="playlist-more" @click="menu">
|
||||
<div style=" margin-top: -1px;
|
||||
margin-left: -5px;
|
||||
width: 36px;
|
||||
height: 36px;">
|
||||
<%- include("../svg/more.svg") %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto flex-center">
|
||||
<button class="more-btn-round" style="float:right;" @click="menu">
|
||||
<div class="svg-icon"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -118,6 +155,20 @@
|
|||
style="width: 50%;">
|
||||
{{data.attributes.copyright}}
|
||||
</div>
|
||||
<hr>
|
||||
<template v-if="typeof data.meta != 'undefined'">
|
||||
<div v-for="view in data.meta.views.order" v-if="data.views[view].data.length != 0">
|
||||
<div class="row" >
|
||||
<div class="col">
|
||||
<h3>{{ data.views[view].attributes.title }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<mediaitem-scroller-horizontal :items="data.views[view].data"></mediaitem-scroller-horizontal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -138,7 +189,8 @@
|
|||
confirm: false,
|
||||
app: this.$root,
|
||||
itemBadges: [],
|
||||
badgesRequested: false
|
||||
badgesRequested: false,
|
||||
headerVisible: true
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
@ -153,6 +205,9 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
isHeaderVisible(visible) {
|
||||
this.headerVisible = visible
|
||||
},
|
||||
getBadges() {
|
||||
return
|
||||
if (this.badgesRequested) {
|
||||
|
@ -171,8 +226,8 @@
|
|||
let friends = badges[id]
|
||||
if (friends) {
|
||||
friends.forEach(function (friend) {
|
||||
self.app.mk.api.socialProfile(friend).then(data => {
|
||||
self.itemBadges.push(data)
|
||||
self.app.mk.api.v3.music(`/v1/social/${app.mk.storefrontId}/social-profiles/${friend}`).then(data => {
|
||||
self.itemBadges.push(data.data.data[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -205,7 +260,7 @@
|
|||
"relate": "library"
|
||||
};
|
||||
const res = await app.mkapi(this.data.attributes.playParams.kind ?? this.data.type, this.data.attributes.playParams.isLibrary ?? false, this.data.attributes.playParams.id ?? this.data.id, params);
|
||||
this.inLibrary = (res && res.attributes && res.attributes.inLibrary) ? res.attributes.inLibrary : false
|
||||
this.inLibrary = (res.data.data[0] && res.data.data[0].attributes && res.data.data[0].attributes.inLibrary) ? res.data.data[0].attributes.inLibrary : false
|
||||
console.log(res)
|
||||
} else {
|
||||
this.inLibrary = true
|
||||
|
@ -229,12 +284,16 @@
|
|||
const params = {"fields[somgs]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library"};
|
||||
var id = this.data.id ?? this.data.attributes.playParams.id
|
||||
const res = await app.mkapi(this.data.attributes.playParams.kind ?? this.data.type, this.data.attributes.playParams.isLibrary ?? false, this.data.attributes.playParams.id ?? this.data.id, params);
|
||||
if (res && res.relationships && res.relationships.library && res.relationships.library.data && res.relationships.library.data.length > 0) {
|
||||
id = res.relationships.library.data[0].id
|
||||
if (res.data.data[0] && res.data.data[0].relationships && res.data.data[0].relationships.library && res.data.data[0].relationships.library.data && res.data.data[0].relationships.library.data.length > 0) {
|
||||
id = res.data.data[0].relationships.library.data[0].id
|
||||
}
|
||||
let kind = this.data.attributes.playParams.kind ?? this.data.type ?? '';
|
||||
const truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
|
||||
app.mk.api.library.remove({[truekind]: id})
|
||||
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
|
||||
{
|
||||
fetchOptions: {
|
||||
method: "DELETE"
|
||||
}})
|
||||
this.inLibrary = false
|
||||
this.confirm = false
|
||||
},
|
||||
|
@ -274,7 +333,19 @@
|
|||
if (!this.data.attributes.canEdit) {
|
||||
return
|
||||
}
|
||||
await app.mk.api.library.putPlaylistTracklisting(this.data.attributes.playParams.id, this.convert())
|
||||
console.log('sds',this.convert())
|
||||
await app.mk.api.v3.music(
|
||||
`/v1/me/library/playlists/${this.data.attributes.playParams.id}/tracks`,
|
||||
{},
|
||||
{
|
||||
fetchOptions: {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
data: this.convert()
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
async remove() {
|
||||
if (!this.data.attributes.canEdit) {
|
||||
|
|
|
@ -14,13 +14,16 @@
|
|||
<mediaitem-square v-else :item="item" :type="getKind(item)"></mediaitem-square>
|
||||
</template>
|
||||
</template>
|
||||
<button v-if="triggerEnabled" style="opacity:0;height: 32px;" v-observe-visibility="{callback: visibilityChanged}">Show More</button>
|
||||
<button v-if="triggerEnabled" style="opacity:0;height: 32px;"
|
||||
v-observe-visibility="{callback: visibilityChanged}">Show More
|
||||
</button>
|
||||
</div>
|
||||
<transition name="fabfade">
|
||||
<button class="top-fab" v-show="showFab" @click="scrollToTop()">
|
||||
<%- include("../svg/arrow-up.svg") %>
|
||||
</button>
|
||||
</transition>
|
||||
<div class="well" v-show="loading"><div class="spinner"></div></div>
|
||||
</div>
|
||||
</script>
|
||||
<script>
|
||||
|
@ -47,16 +50,17 @@
|
|||
canSeeTrigger: false,
|
||||
showFab: false,
|
||||
commonKind: "song",
|
||||
api: this.$root.mk.api
|
||||
api: this.$root.mk.api,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getKind(item) {
|
||||
if(typeof item.kind != "undefined") {
|
||||
if (typeof item.kind != "undefined") {
|
||||
this.commonKind = item.kind;
|
||||
return item.kind
|
||||
}
|
||||
if(typeof item.attributes.playParams != "undefined") {
|
||||
if (typeof item.attributes.playParams != "undefined") {
|
||||
this.commonKind = item.attributes.playParams.kind
|
||||
return item.attributes.playParams.kind
|
||||
}
|
||||
|
@ -71,73 +75,48 @@
|
|||
})
|
||||
},
|
||||
getNext() {
|
||||
// if this.data.next is not null, then we can run this.data.next() and concat to this.data.data to get the next page
|
||||
switch(this.type) {
|
||||
default:
|
||||
case "artists":
|
||||
if (this.data.next && this.triggerEnabled) {
|
||||
this.triggerEnabled = false;
|
||||
|
||||
let nextFn = (data => {
|
||||
console.log(data);
|
||||
this.data.next = data.next;
|
||||
this.data.data = this.data.data.concat(data.data);
|
||||
this.triggerEnabled = true;
|
||||
});
|
||||
if(typeof this.data.next == "function") {
|
||||
this.data.next().then(data => nextFn(data));
|
||||
}else{
|
||||
this.api.v3.music(this.data.next).then(data => nextFn(data));
|
||||
}
|
||||
}else{
|
||||
console.log("No next page");
|
||||
this.triggerEnabled = false;
|
||||
}
|
||||
break;
|
||||
case "search":
|
||||
if (this.data.next && this.triggerEnabled) {
|
||||
this.triggerEnabled = false;
|
||||
this.data.next().then(data => {
|
||||
console.log(data);
|
||||
this.data.next = data[this.data.groups].next;
|
||||
this.data.data = this.data.data.concat(data[this.data.groups].data.data);
|
||||
this.triggerEnabled = true;
|
||||
});
|
||||
}else{
|
||||
console.log("No next page");
|
||||
this.triggerEnabled = false;
|
||||
}
|
||||
break;
|
||||
case "listen_now":
|
||||
case "curator":
|
||||
if (this.data.next && this.triggerEnabled) {
|
||||
this.triggerEnabled = false;
|
||||
app.mk.api.v3.music(this.data.next).then(data => {
|
||||
console.log(data);
|
||||
this.data.next = data.data.next;
|
||||
this.data.data = this.data.data.concat(data.data.data);
|
||||
this.triggerEnabled = true;
|
||||
});
|
||||
}else{
|
||||
console.log("No next page");
|
||||
this.triggerEnabled = false;
|
||||
}
|
||||
break;
|
||||
let self = this
|
||||
this.triggerEnabled = false;
|
||||
if (typeof this.data.next == "undefined") {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
|
||||
this.api.v3.music(this.data.next, app.collectionList.requestBody).then((response) => {
|
||||
console.log(response)
|
||||
if (!app.collectionList.response.groups) {
|
||||
if (response.data.next) {
|
||||
this.data.data = this.data.data.concat(response.data.data);
|
||||
this.data.next = response.data.next;
|
||||
this.triggerEnabled = true;
|
||||
}
|
||||
this.loading = false
|
||||
}else{
|
||||
if(!response.data.results[app.collectionList.response.groups]) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
if (response.data.results[app.collectionList.response.groups].next) {
|
||||
this.data.data = this.data.data.concat(response.data.results[app.collectionList.response.groups].data);
|
||||
this.data.next = response.data.results[app.collectionList.response.groups].next;
|
||||
this.triggerEnabled = true;
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
headerVisibility: function (isVisible, entry) {
|
||||
if(isVisible) {
|
||||
if (isVisible) {
|
||||
this.showFab = false;
|
||||
}else{
|
||||
} else {
|
||||
this.showFab = true;
|
||||
}
|
||||
},
|
||||
visibilityChanged: function (isVisible, entry) {
|
||||
if(isVisible) {
|
||||
if (isVisible) {
|
||||
this.canSeeTrigger = true;
|
||||
this.getNext();
|
||||
}else{
|
||||
} else {
|
||||
this.canSeeTrigger = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,31 +132,21 @@
|
|||
}
|
||||
}
|
||||
if (playlists.length != 0) {
|
||||
this.app.mk.api.playlists(playlists).then(playlistsData => {
|
||||
self.favorites.push(...playlistsData)
|
||||
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/playlists/${playlists.toString()}`).then(playlistsData => {
|
||||
self.favorites.push(...playlistsData.data)
|
||||
})
|
||||
}
|
||||
if (libraryPlaylists.length != 0) {
|
||||
this.app.mk.api.library.playlists(libraryPlaylists).then(playlistsData => {
|
||||
self.favorites.push(...playlistsData)
|
||||
this.app.mk.api.v3.music(`v1/me/library/playlists/${playlists.toString()}`).then(playlistsData => {
|
||||
self.favorites.push(...playlistsData.data)
|
||||
})
|
||||
}
|
||||
},
|
||||
async getArtistFeed() {
|
||||
let artists = this.followedArtists
|
||||
let self = this
|
||||
this.app.mk.api.artists(artists, {
|
||||
"views": "featured-release,full-albums,appears-on-albums,featured-albums,featured-on-albums,singles,compilation-albums,live-albums,latest-release,top-music-videos,similar-artists,top-songs,playlists,more-to-hear,more-to-see",
|
||||
"extend": "artistBio,bornOrFormed,editorialArtwork,editorialVideo,isGroup,origin,hero",
|
||||
"extend[playlists]": "trackCount",
|
||||
"include[songs]": "albums",
|
||||
"fields[albums]": "artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount",
|
||||
"limit[artists:top-songs]": 20,
|
||||
"art[url]": "f"
|
||||
}, {
|
||||
includeResponseMeta: !0
|
||||
}).then(artistData => {
|
||||
artistData.data.forEach(item => {
|
||||
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists?ids=${artists.toString()}&views=latest-release&include[songs]=albums&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount&limit[artists:top-songs]=2&art[url]=f`).then(artistData => {
|
||||
artistData.data.data.forEach(item => {
|
||||
if (item.views["latest-release"].data.length != 0) {
|
||||
self.artistFeed.push(item.views["latest-release"].data[0])
|
||||
}
|
||||
|
@ -176,35 +166,10 @@
|
|||
},
|
||||
async getListenNowData() {
|
||||
let self = this
|
||||
this.app.mk.api.personalRecommendations("", {
|
||||
name: "listen-now",
|
||||
with: "friendsMix,library,social",
|
||||
"art[social-profiles:url]": "c",
|
||||
"art[url]": "c,f",
|
||||
"omit[resource]": "autos",
|
||||
"relate[editorial-items]": "contents",
|
||||
extend: ["editorialCard", "editorialVideo"],
|
||||
"extend[albums]": ["artistUrl"],
|
||||
"extend[library-albums]": ["artistUrl", "editorialVideo"],
|
||||
"extend[playlists]": ["artistNames", "editorialArtwork", "editorialVideo"],
|
||||
"extend[library-playlists]": ["artistNames", "editorialArtwork", "editorialVideo"],
|
||||
"extend[social-profiles]": "topGenreNames",
|
||||
"include[albums]": "artists",
|
||||
"include[songs]": "artists",
|
||||
"include[music-videos]": "artists",
|
||||
"fields[albums]": ["artistName", "artistUrl", "artwork", "contentRating", "editorialArtwork", "editorialVideo", "name", "playParams", "releaseDate", "url"],
|
||||
"fields[artists]": ["name", "url"],
|
||||
"extend[stations]": ["airDate", "supportsAirTimeUpdates"],
|
||||
"meta[stations]": "inflectionPoints",
|
||||
types: "artists,albums,editorial-items,library-albums,library-playlists,music-movies,music-videos,playlists,stations,uploaded-audios,uploaded-videos,activities,apple-curators,curators,tv-shows,social-profiles,social-upsells",
|
||||
platform: "web"
|
||||
}, {
|
||||
includeResponseMeta: !0,
|
||||
reload: !0
|
||||
}).then((data) => {
|
||||
console.log(data.data[1])
|
||||
this.app.mk.api.v3.music(`/v1/me/recommendations?timezone=${encodeURIComponent(app.formatTimezoneOffset())}&name=listen-now&with=friendsMix,library,social&art[social-profiles:url]=c&art[url]=c,f&omit[resource]=autos&relate[editorial-items]=contents&extend=editorialCard,editorialVideo&extend[albums]=artistUrl&extend[library-albums]=artistUrl,editorialVideo&extend[playlists]=artistNames,editorialArtwork,editorialVideo&extend[library-playlists]=artistNames,editorialArtwork,editorialVideo&extend[social-profiles]=topGenreNames&include[albums]=artists&include[songs]=artists&include[music-videos]=artists&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url&fields[artists]=name,url&extend[stations]=airDate,supportsAirTimeUpdates&meta[stations]=inflectionPoints&types=artists,albums,editorial-items,library-albums,library-playlists,music-movies,music-videos,playlists,stations,uploaded-audios,uploaded-videos,activities,apple-curators,curators,tv-shows,social-upsells&platform=web`).then((data) => {
|
||||
console.log(data.data.data[1])
|
||||
try {
|
||||
self.madeForYou = data.data.filter(section => {
|
||||
self.madeForYou = data.data.data.filter(section => {
|
||||
if (section.meta.metrics.moduleType == "6") {
|
||||
return section
|
||||
};
|
||||
|
@ -213,8 +178,8 @@
|
|||
self.sectionsReady.push("madeForYou")
|
||||
|
||||
try {
|
||||
self.recentlyPlayed = data.data[1].relationships.contents.data
|
||||
self.friendsListeningTo = data.data.filter(section => {
|
||||
self.recentlyPlayed = data.data.data[1].relationships.contents.data
|
||||
self.friendsListeningTo = data.data.data.filter(section => {
|
||||
if (section.meta.metrics.moduleType == "11") {
|
||||
return section
|
||||
};
|
||||
|
|
|
@ -1,86 +1,101 @@
|
|||
<script type="text/x-template" id="cider-search">
|
||||
<div class="content-inner">
|
||||
<div class="row">
|
||||
<div class="col-sm" style="width: auto;" v-if="getTopResult()">
|
||||
<template>
|
||||
<h3>Top Result</h3>
|
||||
<mediaitem-square :item="getTopResult()"></mediaitem-square>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else style="text-align: center">
|
||||
<h3>No Results</h3>
|
||||
<p>Try a new search.</p>
|
||||
</div>
|
||||
<div class="col" v-if="search.results.song">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Songs</h3>
|
||||
<div v-if="search != null && search != [] && search.term != ''">
|
||||
<div class="row">
|
||||
<div class="col-sm" style="width: auto;" v-if="getTopResult()">
|
||||
<template>
|
||||
<h3>Top Result</h3>
|
||||
<mediaitem-square :item="getTopResult()"></mediaitem-square>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else style="text-align: center">
|
||||
<h3>No Results</h3>
|
||||
<p>Try a new search.</p>
|
||||
</div>
|
||||
<div class="col" v-if="search.results.song">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Songs</h3>
|
||||
</div>
|
||||
<div class="col-auto flex-center"
|
||||
@click="app.showSearchView(app.search.term, 'song', app.friendlyTypes('song'))"
|
||||
v-if="search.results.song.data.length >= 6">
|
||||
<button class="cd-btn-seeall">See All</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto flex-center"
|
||||
@click="app.showSearchView(app.search.term, 'song', app.friendlyTypes('song'))"
|
||||
v-if="search.results.song.data.length >= 6">
|
||||
<button class="cd-btn-seeall">See All</button>
|
||||
<div>
|
||||
<mediaitem-list-item :item="item" :index="index"
|
||||
v-for="(item, index) in search.results.song.data.limit(6)"></mediaitem-list-item>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<mediaitem-list-item :item="item" :index="index"
|
||||
v-for="(item, index) in search.results.song.data.limit(6)"></mediaitem-list-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="search.results['meta']">
|
||||
<template
|
||||
v-for="section in search.results.meta.results.order" v-if="section != 'song' && section != 'top'">
|
||||
<template v-if="search.results['meta'] != null">
|
||||
<template
|
||||
v-for="section in search.results.meta.results.order" v-if="section != 'song' && section != 'top'">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ app.friendlyTypes(section) }}</h3>
|
||||
</div>
|
||||
<div class="col-auto flex-center" v-if="search.results[section].data.length >= 10">
|
||||
<button class="cd-btn-seeall"
|
||||
@click="app.showSearchView(app.search.term, section, app.friendlyTypes(section))">See
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="!app.friendlyTypes(section).includes('Video')">
|
||||
<mediaitem-scroller-horizontal-large
|
||||
:items="search.results[section].data.limit(10)"></mediaitem-scroller-horizontal-large>
|
||||
</template>
|
||||
<template v-else>
|
||||
<mediaitem-scroller-horizontal-mvview
|
||||
:items="search.results[section].data.limit(10)"></mediaitem-scroller-horizontal-mvview>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="search.resultsSocial.playlist">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ app.friendlyTypes(section) }}</h3>
|
||||
<h3>Shared Playlists</h3>
|
||||
</div>
|
||||
<div class="col-auto flex-center" v-if="search.results[section].data.length >= 10">
|
||||
<div class="col-auto flex-center" v-if="search.resultsSocial.playlist.data.length >= 10">
|
||||
<button class="cd-btn-seeall"
|
||||
@click="app.showSearchView(app.search.term, section, app.friendlyTypes(section))">See
|
||||
All
|
||||
@click="app.showCollection(search.resultsSocial.playlist, 'Shared Playlists', 'default')">See All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="!app.friendlyTypes(section).includes('Video')">
|
||||
<mediaitem-scroller-horizontal-large
|
||||
:items="search.results[section].data.limit(10)"></mediaitem-scroller-horizontal-large>
|
||||
</template>
|
||||
<template v-else>
|
||||
<mediaitem-scroller-horizontal-mvview
|
||||
:items="search.results[section].data.limit(10)"></mediaitem-scroller-horizontal-mvview>
|
||||
</template>
|
||||
<mediaitem-scroller-horizontal-large
|
||||
:items="search.resultsSocial.playlist.data.limit(10)"></mediaitem-scroller-horizontal-large>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="search.resultsSocial.playlist">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Shared Playlists</h3>
|
||||
<template v-if="search.resultsSocial.profile">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>People</h3>
|
||||
</div>
|
||||
<div class="col-auto flex-center" v-if="search.resultsSocial.profile.data.length >= 10">
|
||||
<button class="cd-btn-seeall"
|
||||
@click="app.showCollection(search.resultsSocial.profile, 'People', 'default')">See All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto flex-center" v-if="search.resultsSocial.playlist.data.data.length >= 10">
|
||||
<button class="cd-btn-seeall"
|
||||
@click="app.showCollection(search.resultsSocial.playlist.data, 'Shared Playlists', 'default')">See All
|
||||
</button>
|
||||
<mediaitem-scroller-horizontal-large
|
||||
:items="search.resultsSocial.profile.data.limit(10)"></mediaitem-scroller-horizontal-large>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="categoriesReady || getCategories()">
|
||||
<div>
|
||||
<div class="col" v-if="categoriesView != null && categoriesView != [] && categoriesView[0].attributes != null && categoriesView[0].attributes.title != null">
|
||||
<h3>{{categoriesView[0].attributes.title.stringForDisplay ?? ""}}</h3>
|
||||
</div>
|
||||
<mediaitem-square :kind="'385'" size="600"
|
||||
:item="item ? (item.attributes.kind ? item : ((item.relationships && item.relationships.contents ) ? item.relationships.contents.data[0] : item)) : []"
|
||||
:imagesize="800"
|
||||
v-for="item in categoriesView[1].relationships.contents.data.filter(item => item.type != 'editorial-items')">
|
||||
</div>
|
||||
</div>
|
||||
<mediaitem-scroller-horizontal-large
|
||||
:items="search.resultsSocial.playlist.data.data.limit(10)"></mediaitem-scroller-horizontal-large>
|
||||
</template>
|
||||
<template v-if="search.resultsSocial.profile">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>People</h3>
|
||||
</div>
|
||||
<div class="col-auto flex-center" v-if="search.resultsSocial.profile.data.data.length >= 10">
|
||||
<button class="cd-btn-seeall"
|
||||
@click="app.showCollection(search.resultsSocial.profile.data, 'People', 'default')">See All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mediaitem-scroller-horizontal-large
|
||||
:items="search.resultsSocial.profile.data.data.limit(10)"></mediaitem-scroller-horizontal-large>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
@ -90,7 +105,9 @@
|
|||
props: ['search'],
|
||||
data: function () {
|
||||
return {
|
||||
app: this.$root
|
||||
app: this.$root,
|
||||
categoriesView : [],
|
||||
categoriesReady : false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -100,6 +117,13 @@
|
|||
} catch( error ) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
async getCategories() {
|
||||
if(this.categoriesView != [] && this.categoriesView.length > 0) {this.categoriesReady = true; return await true;} else {
|
||||
let response = await this.app.mk.api.v3.music(`/v1/recommendations/${this.app.mk.storefrontId}?timezone=${encodeURIComponent(this.app.formatTimezoneOffset())}&name=search-landing&platform=web&extend=editorialArtwork&art%5Burl%5D=f%2Cc&types=editorial-items%2Capple-curators%2Cactivities`);
|
||||
this.categoriesView = response.data.data;
|
||||
this.categoriesReady = true;
|
||||
return await true;}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5,5 +5,8 @@
|
|||
{{ $store.state.test }}
|
||||
<div class="spinner"></div>
|
||||
<button class="md-btn">Cider Button</button>
|
||||
<div style="position: relative;width: 300px;height: 300px;">
|
||||
<artwork-material url="https://is3-ssl.mzstatic.com/image/thumb/Music126/v4/13/41/13/1341133b-560f-1aee-461f-c4b32ec049b4/cover.jpg/{w}x{h}bb.jpg"></artwork-material>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
22
src/web-remote/assets/Grabber.svg
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 28 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-11,-10)">
|
||||
<path d="M39,12.24C39,11.004 37.996,10 36.76,10L13.24,10C12.004,10 11,11.004 11,12.24L11,71.76C11,72.996 12.004,74 13.24,74L36.76,74C37.996,74 39,72.996 39,71.76L39,12.24Z" style="fill:rgb(108,108,108);fill-opacity:0.43;"/>
|
||||
<g transform="matrix(0.714286,0,0,1,7.14286,0)">
|
||||
<rect x="18" y="41" width="14" height="2" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.714286,0,0,1,7.14286,-5)">
|
||||
<rect x="18" y="41" width="14" height="2" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.714286,0,0,1,7.14286,5)">
|
||||
<rect x="18" y="41" width="14" height="2" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.571429,0,0,0.6,10.7143,10.4)">
|
||||
<path d="M25,26L32,36L18,36L25,26Z" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.571429,0,0,-0.6,10.7143,73.6)">
|
||||
<path d="M25,26L32,36L18,36L25,26Z" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
src/web-remote/assets/arrow-left.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M257.5 445.1l-22.2 22.2c-9.4 9.4-24.6 9.4-33.9 0L7 273c-9.4-9.4-9.4-24.6 0-33.9L201.4 44.7c9.4-9.4 24.6-9.4 33.9 0l22.2 22.2c9.5 9.5 9.3 25-.4 34.3L136.6 216H424c13.3 0 24 10.7 24 24v32c0 13.3-10.7 24-24 24H136.6l120.5 114.8c9.8 9.3 10 24.8.4 34.3z"/></svg>
|
After Width: | Height: | Size: 521 B |
1
src/web-remote/assets/backward.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M11.5 280.6l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-27.4-31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 49.2zm256 0l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-27.4-31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 49.2z"/></svg>
|
After Width: | Height: | Size: 500 B |
1
src/web-remote/assets/forward.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M500.5 231.4l-192-160C287.9 54.3 256 68.6 256 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2zm-256 0l-192-160C31.9 54.3 0 68.6 0 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2z"/></svg>
|
After Width: | Height: | Size: 493 B |
1
src/web-remote/assets/infinity.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="white"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M471.1 96C405 96 353.3 137.3 320 174.6 286.7 137.3 235 96 168.9 96 75.8 96 0 167.8 0 256s75.8 160 168.9 160c66.1 0 117.8-41.3 151.1-78.6 33.3 37.3 85 78.6 151.1 78.6 93.1 0 168.9-71.8 168.9-160S564.2 96 471.1 96zM168.9 320c-40.2 0-72.9-28.7-72.9-64s32.7-64 72.9-64c38.2 0 73.4 36.1 94 64-20.4 27.6-55.9 64-94 64zm302.2 0c-38.2 0-73.4-36.1-94-64 20.4-27.6 55.9-64 94-64 40.2 0 72.9 28.7 72.9 64s-32.7 64-72.9 64z"/></svg>
|
After Width: | Height: | Size: 684 B |
1
src/web-remote/assets/list.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"/></svg>
|
After Width: | Height: | Size: 831 B |
1
src/web-remote/assets/pause.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M144 479H48c-26.5 0-48-21.5-48-48V79c0-26.5 21.5-48 48-48h96c26.5 0 48 21.5 48 48v352c0 26.5-21.5 48-48 48zm304-48V79c0-26.5-21.5-48-48-48h-96c-26.5 0-48 21.5-48 48v352c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48z"/></svg>
|
After Width: | Height: | Size: 487 B |
1
src/web-remote/assets/play.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"/></svg>
|
After Width: | Height: | Size: 384 B |
1
src/web-remote/assets/quote-right.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M464 32H336c-26.5 0-48 21.5-48 48v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48zm-288 0H48C21.5 32 0 53.5 0 80v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48z"/></svg>
|
After Width: | Height: | Size: 640 B |
47
src/web-remote/assets/repeat.svg
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 60 60" style="enable-background:new 0 0 60 60;" xml:space="preserve">
|
||||
<g>
|
||||
<path fill="white" d="M42,12H20.414l7.293-7.293c0.391-0.391,0.391-1.023,0-1.414s-1.023-0.391-1.414,0l-8.999,8.999
|
||||
c-0.093,0.092-0.166,0.203-0.217,0.326c-0.101,0.244-0.101,0.52,0,0.764c0.051,0.123,0.124,0.234,0.217,0.326l8.999,8.999
|
||||
C26.488,22.902,26.744,23,27,23s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414L20.414,14H42c8.822,0,16,7.178,16,16
|
||||
c0,4.252-1.668,8.264-4.696,11.295c-0.391,0.391-0.391,1.024,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
|
||||
C58.124,39.3,60,34.786,60,30C60,20.075,51.925,12,42,12z"/>
|
||||
<path fill="white" d="M35.707,37.293c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414L41.586,46H18C9.178,46,2,38.822,2,30
|
||||
c0-3.783,1.359-7.46,3.828-10.354c0.358-0.421,0.309-1.052-0.111-1.41c-0.419-0.359-1.052-0.31-1.41,0.111
|
||||
C1.529,21.604,0,25.741,0,30c0,9.925,8.075,18,18,18h23.586l-7.293,7.293c-0.391,0.391-0.391,1.023,0,1.414
|
||||
C34.488,56.902,34.744,57,35,57s0.512-0.098,0.707-0.293l9-9c0.391-0.391,0.391-1.023,0-1.414L35.707,37.293z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
src/web-remote/assets/search.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"/></svg>
|
After Width: | Height: | Size: 618 B |
45
src/web-remote/assets/shuffle.svg
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 230.055 230.055" style="enable-background:new 0 0 230.055 230.055;" xml:space="preserve">
|
||||
<path fill="white" d="M199.419,124.497c-3.516-3.515-9.213-3.515-12.729,0c-3.515,3.515-3.515,9.213,0,12.728l12.637,12.636h-8.406
|
||||
c-8.177,0-16.151-2.871-22.453-8.083l-32.346-26.751l32.345-26.751c6.303-5.212,14.277-8.083,22.454-8.083h8.406L186.69,92.83
|
||||
c-3.515,3.515-3.515,9.213,0,12.728c1.758,1.757,4.061,2.636,6.364,2.636s4.606-0.879,6.364-2.636l28-28
|
||||
c3.515-3.515,3.515-9.213,0-12.728l-28-28c-3.516-3.515-9.213-3.515-12.729,0c-3.515,3.515-3.515,9.213,0,12.728l12.637,12.636
|
||||
h-8.406c-12.354,0-24.403,4.337-33.926,12.211L122,103.348L82.564,70.733c-6.658-5.507-15.084-8.54-23.724-8.54H9
|
||||
c-4.971,0-9,4.029-9,9s4.029,9,9,9h49.841c4.462,0,8.813,1.566,12.252,4.411l36.786,30.423L71.094,145.45
|
||||
c-3.439,2.844-7.791,4.411-12.253,4.411H9c-4.971,0-9,4.029-9,9s4.029,9,9,9h49.841c8.64,0,17.065-3.033,23.725-8.54L122,126.707
|
||||
l34.996,28.943c9.521,7.875,21.57,12.211,33.925,12.211h8.406l-12.637,12.636c-3.515,3.515-3.515,9.213,0,12.728
|
||||
c1.758,1.757,4.061,2.636,6.364,2.636s4.606-0.879,6.364-2.636l28-28c3.515-3.515,3.515-9.213,0-12.728L199.419,124.497z"/>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
src/web-remote/assets/volume-down.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M215.03 72.04L126.06 161H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V89.02c0-21.47-25.96-31.98-40.97-16.98zm123.2 108.08c-11.58-6.33-26.19-2.16-32.61 9.45-6.39 11.61-2.16 26.2 9.45 32.61C327.98 229.28 336 242.62 336 257c0 14.38-8.02 27.72-20.92 34.81-11.61 6.41-15.84 21-9.45 32.61 6.43 11.66 21.05 15.8 32.61 9.45 28.23-15.55 45.77-45 45.77-76.88s-17.54-61.32-45.78-76.87z"/></svg>
|
After Width: | Height: | Size: 710 B |
1
src/web-remote/assets/volume-up.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M215.03 71.05L126.06 160H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V88.02c0-21.46-25.96-31.98-40.97-16.97zm233.32-51.08c-11.17-7.33-26.18-4.24-33.51 6.95-7.34 11.17-4.22 26.18 6.95 33.51 66.27 43.49 105.82 116.6 105.82 195.58 0 78.98-39.55 152.09-105.82 195.58-11.17 7.32-14.29 22.34-6.95 33.5 7.04 10.71 21.93 14.56 33.51 6.95C528.27 439.58 576 351.33 576 256S528.27 72.43 448.35 19.97zM480 256c0-63.53-32.06-121.94-85.77-156.24-11.19-7.14-26.03-3.82-33.12 7.46s-3.78 26.21 7.41 33.36C408.27 165.97 432 209.11 432 256s-23.73 90.03-63.48 115.42c-11.19 7.14-14.5 22.07-7.41 33.36 6.51 10.36 21.12 15.14 33.12 7.46C447.94 377.94 480 319.54 480 256zm-141.77-76.87c-11.58-6.33-26.19-2.16-32.61 9.45-6.39 11.61-2.16 26.2 9.45 32.61C327.98 228.28 336 241.63 336 256c0 14.38-8.02 27.72-20.92 34.81-11.61 6.41-15.84 21-9.45 32.61 6.43 11.66 21.05 15.8 32.61 9.45 28.23-15.55 45.77-45 45.77-76.88s-17.54-61.32-45.78-76.86z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1044
src/web-remote/ciderframework.css
Normal file
BIN
src/web-remote/icon-192x192.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/web-remote/icon-256x256.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/web-remote/icon-384x384.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
src/web-remote/icon-512x512.png
Normal file
After Width: | Height: | Size: 38 KiB |
|
@ -1 +1,784 @@
|
|||
Web Remote
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>Web Remote</title>
|
||||
<link rel="stylesheet" href="style.css?v=2">
|
||||
<script src="vue.js"></script>
|
||||
<script src="sortable.min.js"></script>
|
||||
<script src="vuedraggable.umd.min.js"></script>
|
||||
<link rel="manifest" href="./manifest.json?v=2">
|
||||
</head>
|
||||
|
||||
<body oncontextmenu="return false;">
|
||||
<div id="app" :style="{'--artwork': getAlbumArtUrl()}">
|
||||
<!-- App view when connected -->
|
||||
<template v-if="connectedState == 1">
|
||||
<!-- Streamer Overlay -->
|
||||
<template></template>
|
||||
<!-- Mini Player -->
|
||||
<template v-if="screen == 'miniplayer'">
|
||||
<div class="miniplayer-main">
|
||||
<div class="media-artwork--miniplayer" :class="artworkPlaying()"
|
||||
:style="{'--artwork': getAlbumArtUrl()}">
|
||||
</div>
|
||||
<div class="miniplayer-draggable">
|
||||
|
||||
</div>
|
||||
<div class="miniplayer-controls">
|
||||
<button class="md-btn playback-button--small repeat" @click="repeat()"
|
||||
v-if="player.currentMediaItem.repeatMode == 0"></button>
|
||||
<button class="md-btn playback-button--small repeat active" @click="repeat()"
|
||||
v-else-if="player.currentMediaItem.repeatMode == 2"></button>
|
||||
<button class="md-btn playback-button--small repeat repeatOne" @click="repeat()"
|
||||
v-else-if="player.currentMediaItem.repeatMode == 1"></button>
|
||||
<button class="md-btn playback-button previous" @click="previous()"></button>
|
||||
<button class="md-btn playback-button pause" @click="pause()"
|
||||
v-if="player.currentMediaItem.status"></button>
|
||||
<button class="md-btn playback-button play" @click="play()" v-else></button>
|
||||
<button class="md-btn playback-button next" @click="next()"></button>
|
||||
<button class="md-btn playback-button--small shuffle" @click="shuffle()"
|
||||
v-if="player.currentMediaItem.shuffleMode == 0"></button>
|
||||
<button class="md-btn playback-button--small shuffle active" @click="shuffle()"
|
||||
v-else></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Player -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel player-panel" v-if="screen == 'player'">
|
||||
<div class="player_top">
|
||||
<div class="md-body player-artwork-container">
|
||||
<div class="media-artwork" :class="artworkPlaying()" :style="{'--artwork': getAlbumArtUrl()}">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player_bottom" v-if="player.lowerPanelState == 'lyrics'">
|
||||
<div class="md-header" style="width:100%;">
|
||||
<div class="list-entry" v-if="false">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ player.currentMediaItem.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
{{ player.currentMediaItem.artistName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-body lyric-body" style="width:100%;">
|
||||
<template v-if="player.lyrics">
|
||||
<template v-for="lyric in player.lyrics" v-if="lyric.line != 'lrcInstrumental'">
|
||||
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
|
||||
:class="getLyricClass(lyric.startTime, lyric.endTime)">
|
||||
{{ lyric.line }}
|
||||
</h3>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
|
||||
:class="getLyricClass(lyric.startTime, lyric.endTime)">
|
||||
<div class="lyricWaiting">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</h3>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
No Lyrics Available
|
||||
</template>
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
<button class="md-btn playback-button--small lyrics active" @click="player.lowerPanelState = 'controls'"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player_bottom" v-if="player.lowerPanelState == 'controls'">
|
||||
<div class="md-footer">
|
||||
<div class="row player-track-info">
|
||||
<div class="col nopadding text-overflow-elipsis">
|
||||
<div class="player-song-title text-overflow-elipsis">
|
||||
{{ player.currentMediaItem.name }}
|
||||
</div>
|
||||
<div class="player-song-artist text-overflow-elipsis" @click="searchArtist()">
|
||||
{{ player.currentMediaItem.artistName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto nopadding player-more-container" v-if="false" style="">
|
||||
<button @click="player.songActions = true;" class="player-more-button">...</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
<input type="range" min="0" :value="player.currentMediaItem.durationInMillis - player.currentMediaItem.remainingTime" :max="player.currentMediaItem.durationInMillis" class="web-slider playback-slider" @input="seekTo($event.target.value)">
|
||||
<div class="row nopadding player-duration-container" style="width: 90%;margin: 0 auto;">
|
||||
<div class="col nopadding player-duration-time" style="text-align:left">
|
||||
{{ parseTime(player.currentMediaItem.durationInMillis - player.currentMediaItem.remainingTime) }}
|
||||
</div>
|
||||
<div class="col nopadding player-duration-time" style="text-align:right">
|
||||
-{{ parseTime(player.currentMediaItem.remainingTime) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-footer playback-buttons">
|
||||
<button class="md-btn playback-button--small repeat" @click="repeat()" v-if="player.currentMediaItem.repeatMode == 0"></button>
|
||||
<button class="md-btn playback-button--small repeat active" @click="repeat()" v-else-if="player.currentMediaItem.repeatMode == 2"></button>
|
||||
<button class="md-btn playback-button--small repeat repeatOne" @click="repeat()" v-else-if="player.currentMediaItem.repeatMode == 1"></button>
|
||||
<button class="md-btn playback-button previous" @click="previous()"></button>
|
||||
<button class="md-btn playback-button pause" @click="pause()" v-if="player.currentMediaItem.status"></button>
|
||||
<button class="md-btn playback-button play" @click="play()" v-else></button>
|
||||
<button class="md-btn playback-button next" @click="next()"></button>
|
||||
<button class="md-btn playback-button--small shuffle" @click="shuffle()" v-if="player.currentMediaItem.shuffleMode == 0"></button>
|
||||
<button class="md-btn playback-button--small shuffle active" @click="shuffle()" v-else></button>
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
<div class="row volume-slider-container">
|
||||
<div class="col-auto">
|
||||
<div class="player-volume-glyph decrease"></div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="range" class="web-slider volume-slider" max="1" min="0" step="0.01" @input="setVolume($event.target.value)" :value="player.currentMediaItem.volume">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="player-volume-glyph increase"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'portrait'" @click="showLyrics()"></button>
|
||||
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'landscape'" @click="showLyricsInline()"></button>
|
||||
<button class="md-btn playback-button--small queue" @click="showQueue()"></button>
|
||||
<button class="md-btn playback-button--small search" @click="showSearch()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Search -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel search-panel" v-if="screen == 'search'">
|
||||
<div class="search-header">
|
||||
<div class="md-header">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<button class="back-button" @click="screen = 'player'"></button>
|
||||
</div>
|
||||
<div class="col" style="display: flex;align-items: center;">
|
||||
<div class="col">
|
||||
<input type="text" placeholder="Artists, Songs, Lyrics, and More" spellcheck="false" v-model="search.query" @change="searchQuery()" v-on:keyup.enter="searchQuery()" class="search-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-header search-type-container">
|
||||
<button class="search-type-button" @click="search.searchType = 'applemusic';searchQuery()" :class="searchTypeClass('applemusic')" style="width:100%;">Apple Music
|
||||
</button>
|
||||
<button class="search-type-button" @click="search.searchType = 'library';searchQuery()" :class="searchTypeClass('library')" style="width:100%;">Library
|
||||
</button>
|
||||
</div>
|
||||
<div class="md-header search-tab-container" v-if="search.state == 2">
|
||||
<button class="search-tab" @click="search.tab = 'all'" :class="searchTabClass('all')">All
|
||||
Results
|
||||
</button>
|
||||
<button class="search-tab" @click="search.tab = 'songs'" :class="searchTabClass('songs')">Songs
|
||||
</button>
|
||||
<button class="search-tab" @click="search.tab = 'albums'" :class="searchTabClass('albums')">Albums
|
||||
</button>
|
||||
<button class="search-tab" @click="search.tab = 'artists'" :class="searchTabClass('artists')">Artists
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-body-container">
|
||||
<transition name="wpfade">
|
||||
<div class="md-body search-body" v-if="search.state == 0">
|
||||
<div style="font-size: 17px;display:flex;flex-direction: column;justify-content: center;align-items: center;">
|
||||
<img src="./assets/search.svg" style="width: 40px;margin: 32px;opacity: 0.85"> Search by song, album, artist, or lyrics.
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="wpfade">
|
||||
<div class="md-body search-body" v-if="search.state == 1">
|
||||
<!-- loading state -->
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="wpfade">
|
||||
<div class="md-body search-body" ref="searchBody" @scroll="searchScroll" style="overflow-y:auto;" v-if="search.state == 2">
|
||||
<template v-if="canShowSearchTab('songs')">
|
||||
<div class="list-entry-header">Songs</div>
|
||||
|
||||
<div class="list-entry" v-for="song in search.results.songs"
|
||||
@click="trackSelect(song)">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image" v-if="song.artwork"
|
||||
:style="{'--artwork': getAlbumArtUrlList(song.artwork.url)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ song.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
{{ song.artistName }}
|
||||
<span class="lossless-badge" v-if="song.audioTraits.includes('lossless')">Lossless</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-entry" v-for="song in search.results['library-songs']"
|
||||
@click="trackSelect(song)">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image" v-if="song.artwork"
|
||||
:style="{'--artwork': getAlbumArtUrlList(song.artwork.url)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ song.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
{{ song.artistName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template v-if="canShowSearchTab('albums')">
|
||||
<div class="list-entry-header">Albums</div>
|
||||
|
||||
<div class="list-entry" v-for="album in search.results.albums"
|
||||
@click="showAlbum(album.id)">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image" v-if="album.artwork"
|
||||
:style="{'--artwork': getAlbumArtUrlList(album.artwork.url)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ album.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
{{ album.artistName }}
|
||||
<span class="lossless-badge" v-if="album.audioTraits.includes('lossless')">Lossless</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-entry" v-for="album in search.results['library-albums']">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image" v-if="album.artwork"
|
||||
:style="{'--artwork': getAlbumArtUrlList(album.artwork.url)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ album.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
{{ album.artistName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template v-if="canShowSearchTab('artists')">
|
||||
<div class="list-entry-header">Artists</div>
|
||||
|
||||
<div class="list-entry"
|
||||
@click="showArtist(artist.id)"
|
||||
v-for="artist in search.results.artists"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image artist" v-if="artist.artwork"
|
||||
:style="{'--artwork': getAlbumArtUrlList(artist.artwork.url)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ artist.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-entry" v-for="artist in search.results['library-artists']">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image artist" v-if="artist.artwork"
|
||||
:style="{'--artwork': getAlbumArtUrlList(artist.artwork.url)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ artist.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<footer-player></footer-player>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Track Select Actions -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel context-menu" style="overflow-y:auto;" v-if="search.trackSelect">
|
||||
<div class="md-body context-menu-body">
|
||||
<button class="context-menu-item context-menu-item--left" @click="playMediaItemById(search.selected.id);clearSelectedTrack()">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center" v-if="search.selected.artwork"
|
||||
style="display:flex;align-items: center;">
|
||||
<div class="list-entry-image"
|
||||
:style="{'--artwork': getAlbumArtUrlList(search.selected.artwork.url)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center" style="display:flex;align-items: center;">
|
||||
<div style="width:100%;font-size: 18px;">
|
||||
{{ search.selected.name }}
|
||||
</div>
|
||||
<div style="width:100%;font-size: 16px;">
|
||||
{{ search.selected.artistName }}
|
||||
</div>
|
||||
<div style="width:100%;font-size: 14px;">
|
||||
{{ parseTime(search.selected.durationInMillis) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="md-body context-menu-body" style="height: auto;">
|
||||
<button class="context-menu-item context-menu-item--left" @click="playMediaItemById(search.selected.id);clearSelectedTrack()">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Play
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
▶️
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="context-menu-item context-menu-item--left" @click="playNext('song', search.selected.id);clearSelectedTrack()">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Play Next
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
⏭️
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="context-menu-item context-menu-item--left" @click="playLater('song', search.selected.id);clearSelectedTrack()">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Play Later
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
⌛
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="context-menu-item context-menu-item--left" v-if="false">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Open in {{ musicAppVariant() }}
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
🎵
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
<button class="context-menu-item" @click="clearSelectedTrack()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Song Actions -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel context-menu" v-if="player.songActions">
|
||||
<div class="md-header">
|
||||
|
||||
</div>
|
||||
<div class="md-body context-menu-body">
|
||||
<button class="context-menu-item context-menu-item--left" v-if="false">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Add To Library
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
➕
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="context-menu-item context-menu-item--left" v-if="false">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Love
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
❤️
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="context-menu-item context-menu-item--left">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Share
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
🌐
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="context-menu-item context-menu-item--left">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Open in {{ musicAppVariant() }}
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
🎵
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
<button class="context-menu-item" @click="player.songActions = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Artist Page -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel" v-if="screen == 'artist-page'" v-if="artistPage.data['name']">
|
||||
<div class="md-header">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<button class="back-button" @click="showSearch(true)"></button>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
{{ artistPage.data["name"] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="album-body-container" :style="getMediaPalette(artistPage.data)">
|
||||
<div class="artist-header" v-if="artistPage.data['artwork']" :style="getMediaPalette(artistPage.data)">
|
||||
<div class="artist-header-portrait" :style="{'--artwork': getAlbumArtUrlList(artistPage.data['artwork']['url'], 600)}"></div>
|
||||
<h2>{{ artistPage.data["name"] }}</h2>
|
||||
</div>
|
||||
<div class="md-body artist-body">
|
||||
<h2>Songs</h2>
|
||||
<div class="song-scroller-horizontal">
|
||||
<button v-for="song in artistPage.data['songs']" class="song-placeholder" @click="trackSelect(song)">
|
||||
{{ song.name }}
|
||||
</button>
|
||||
</div>
|
||||
<h2>Albums</h2>
|
||||
<div class="mediaitem-scroller-horizontal">
|
||||
<button v-for="album in artistPage.data['albums']" class="album-placeholder" @click="showAlbum(album.id)">
|
||||
{{ album.name }}
|
||||
</button>
|
||||
</div>
|
||||
<h2>Playlists</h2>
|
||||
<div class="mediaitem-scroller-horizontal">
|
||||
<button v-for="playlist in artistPage.data['playlists']" class="album-placeholder">
|
||||
{{ playlist.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer-player></footer-player>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Queue -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel" v-if="screen == 'queue'">
|
||||
<div class="md-header">
|
||||
<div class="list-entry" @click="screen = 'player'">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ player.currentMediaItem.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
{{ player.currentMediaItem.artistName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-header" style="text-align: right;padding: 5px 16px;">
|
||||
<button class="md-btn playback-button--small autoplay" v-if="!player.currentMediaItem.autoplayEnabled" @click="setAutoplay(true)"></button>
|
||||
<button class="md-btn playback-button--small autoplay activeColor" v-else @click="setAutoplay(false)"></button>
|
||||
</div>
|
||||
<div class="md-body queue-body" v-if="!player.queue['_queueItems']">
|
||||
Empty
|
||||
</div>
|
||||
<div class="md-body queue-body" style="overflow-y:auto;" id="list-queue" v-else>
|
||||
<draggable v-model="queue.temp" handle=".handle" filter=".passed" @change="queueMove">
|
||||
<template v-for="(song, position) in queue.temp" v-if="position > player.queue['_position']">
|
||||
<div class="list-entry" :class="getQueuePositionClass(position)">
|
||||
<div class="row" style="width:100%;">
|
||||
<div class="col-auto">
|
||||
<div class="handle">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image" v-if="song.item.attributes.artwork"
|
||||
:style="{'--artwork': getAlbumArtUrlList(song.item.attributes.artwork.url)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ song.item.attributes.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
{{ song.item.attributes.artistName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto flex-center" style="text-align:right;">
|
||||
<div v-if="position == player.queue['_position']">▶️</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'portrait'" @click="showLyrics()"></button>
|
||||
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'landscape'" @click="screen = 'player';showLyricsInline()"></button>
|
||||
<button class="md-btn playback-button--small queue active" @click="screen = 'player'"></button>
|
||||
<button class="md-btn playback-button--small search" @click="showSearch()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Lyrics -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel" v-if="screen == 'lyrics'">
|
||||
<div class="md-header">
|
||||
<div class="list-entry" @click="screen = 'player'">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ player.currentMediaItem.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
{{ player.currentMediaItem.artistName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-body lyric-body">
|
||||
<template v-if="player.lyrics">
|
||||
<template v-for="lyric in player.lyrics" v-if="lyric.line != 'lrcInstrumental'">
|
||||
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
|
||||
:class="getLyricClass(lyric.startTime, lyric.endTime)">
|
||||
{{ lyric.line }}
|
||||
</h3>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
|
||||
:class="getLyricClass(lyric.startTime, lyric.endTime)">
|
||||
<div class="lyricWaiting">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</h3>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
No Lyrics Available
|
||||
</template>
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
<button class="md-btn playback-button--small lyrics active" @click="screen = 'player'"></button>
|
||||
<button class="md-btn playback-button--small queue" @click="showQueue()"></button>
|
||||
<button class="md-btn playback-button--small search" @click="showSearch()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Album Page -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel md-container_album" v-if="screen == 'album-page' && albumPage.data['name']">
|
||||
<div class="md-header">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<button class="back-button" @click="showSearch(true)"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="album-body-container">
|
||||
<div class="md-header">
|
||||
<div class="albumpage-artwork" :style="{'--artwork': getAlbumArtUrlList(albumPage.data['artwork']['url'], 300)}">
|
||||
</div>
|
||||
<div class="albumpage-album-name">
|
||||
{{ albumPage.data["name"] }}
|
||||
</div>
|
||||
<div class="albumpage-artist-name" @click="showArtist(albumPage.data['artists'][0]['id'])">
|
||||
{{ albumPage.data["artistName"] }}
|
||||
</div>
|
||||
<div class="albumpage-misc-info">
|
||||
{{ albumPage.data.genreNames[0] }} ∙ {{ new Date(albumPage.data.releaseDate).getFullYear() }}
|
||||
</div>
|
||||
<div class="row" style="margin-top: 20px;">
|
||||
<div class="col">
|
||||
<button class="wr-btn" @click="playAlbum(albumPage.data.id, false)" style="width:100%;">Play
|
||||
</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button class="wr-btn" style="width:100%;" @click="playAlbum(albumPage.data.id, true)">Shuffle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="albumpage-album-notes" v-if="albumPage.data['editorialNotes']">
|
||||
<div class="notes-preview" v-html="albumPage.data['editorialNotes']['standard']">
|
||||
</div>
|
||||
<button @click="albumPage.editorsNotes = true" class="notes-more">More</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="md-body artist-body">
|
||||
<div class="list-entry-header">Tracks</div>
|
||||
<div class="list-entry" v-for="song in albumPage.data['tracks']" @click="trackSelect(song)">
|
||||
<div class="row">
|
||||
<div class="col-auto flex-center">
|
||||
<div class="list-entry-image" v-if="song.artwork" :style="{'--artwork': getAlbumArtUrlList(song.artwork.url)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center">
|
||||
<div class="list-entry-name">
|
||||
{{ song.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist">
|
||||
{{ song.artistName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
<div>{{ albumPage.data['tracks'].length }} Tracks</div>
|
||||
<div>
|
||||
{{ albumPage.data['copyright'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer-player></footer-player>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Album Page - Editorial Notes -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel context-menu" v-if="albumPage.editorsNotes" style="padding-top: 42px;">
|
||||
<div class="md-header" :style="getMediaPalette(albumPage.data)" style="font-size: 18px;background:var(--bgColor);color:var(--textColor1);text-align: center;border-radius: 10px 10px 0 0;border-top: 1px solid #ffffff1f;">
|
||||
{{ albumPage.data["name"] }}
|
||||
</div>
|
||||
<div class="md-body album-page-fullnotes-body" :style="getMediaPalette(albumPage.data)" style="background:var(--bgColor);color:var(--textColor1);" v-html="albumPage.data['editorialNotes']['standard']">
|
||||
</div>
|
||||
<div class="md-footer" :style="getMediaPalette(albumPage.data)" style="background:var(--bgColor);color:var(--textColor1);">
|
||||
<button class="context-menu-item" @click="albumPage.editorsNotes = false">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<!-- Loading -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel connection-error-panel" v-if="connectedState != 1">
|
||||
<div class="md-header">
|
||||
|
||||
</div>
|
||||
<div class="md-body" style="display:flex;justify-content: center;align-items: center;">
|
||||
<div v-if="connectedState == 0">
|
||||
Loading...
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3 style="text-align:center;">Connection Interrupted</h3>
|
||||
<!--<button class="md-btn md-btn-primary" style="font-weight:500;width: 120px;border-radius: 50px;display:block;margin: 0 auto;" @click="connect()">Retry-->
|
||||
<button class="md-btn md-btn-primary" style="font-weight:500;width: 120px;border-radius: 50px;display:block;margin: 0 auto;" onclick="document.location = document.location">Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Template -->
|
||||
<transition name="wpfade">
|
||||
<div class="md-container md-container_panel" v-if="false">
|
||||
<div class="md-header">
|
||||
|
||||
</div>
|
||||
<div class="md-body">
|
||||
|
||||
</div>
|
||||
<div class="md-footer">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<script type="text/x-template" id="footer-player">
|
||||
<div class="footer-player" v-show="$parent.player.currentMediaItem['name']">
|
||||
<div class="row" style="width:100%;margin:0px;">
|
||||
<div class="col-auto flex-center" style="padding:0 6px;" @click="$parent.screen = 'player'">
|
||||
<div class="list-entry-image" :style="{'--artwork': $parent.getAlbumArtUrl()}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-center text-overflow-elipsis" @click="$parent.screen = 'player'">
|
||||
<div class="list-entry-name text-overflow-elipsis">
|
||||
{{ $parent.player.currentMediaItem.name }}
|
||||
</div>
|
||||
<div class="list-entry-artist text-overflow-elipsis">
|
||||
{{ $parent.player.currentMediaItem.artistName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="md-btn playback-button pause" @click="$parent.pause()" v-if="$parent.player.currentMediaItem.status"></button>
|
||||
<button class="md-btn playback-button play" @click="$parent.play()" v-else></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script src="./index.js?v=1"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
618
src/web-remote/index.js
Normal file
|
@ -0,0 +1,618 @@
|
|||
var socket;
|
||||
|
||||
Vue.component('footer-player', {
|
||||
template: '#footer-player'
|
||||
});
|
||||
|
||||
// vue instance
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
screen: "player",
|
||||
player: {
|
||||
currentMediaItem: {},
|
||||
songActions: false,
|
||||
lyrics: {},
|
||||
lyricsMediaItem: {},
|
||||
lyricsDebug: {
|
||||
current: 0,
|
||||
start: 0,
|
||||
end: 0
|
||||
},
|
||||
queue: {},
|
||||
lowerPanelState: "controls",
|
||||
userInteraction: false
|
||||
},
|
||||
queue: {
|
||||
temp: []
|
||||
},
|
||||
artistPage: {
|
||||
data: {},
|
||||
editorsNotes: false
|
||||
},
|
||||
albumPage: {
|
||||
data: {},
|
||||
editorsNotes: false
|
||||
},
|
||||
search: {
|
||||
query: "",
|
||||
results: [],
|
||||
state: 0,
|
||||
tab: "all",
|
||||
searchType: "applemusic",
|
||||
trackSelect: false,
|
||||
selected: {},
|
||||
queue: {},
|
||||
lastPage: "search",
|
||||
lastY: 0
|
||||
},
|
||||
lastPage: "player",
|
||||
connectedState: 0,
|
||||
url: window.location.hostname,
|
||||
mode: "default",
|
||||
// url: "localhost",
|
||||
},
|
||||
methods: {
|
||||
searchScroll(e) {
|
||||
this.search.lastY = e.target.scrollTop;
|
||||
},
|
||||
musicKitAPI(method, id, params) {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
action: "musickit-api",
|
||||
method: method,
|
||||
id: id,
|
||||
params: params
|
||||
})
|
||||
)
|
||||
},
|
||||
resetPlayerUI() {
|
||||
this.player.lowerPanelState = "controls";
|
||||
},
|
||||
musicAppVariant() {
|
||||
if (navigator.userAgent.match(/Android/i)) {
|
||||
return "Cider";
|
||||
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
|
||||
return "Cider";
|
||||
} else {
|
||||
if (navigator.userAgent.indexOf('Mac') > 0) {
|
||||
return 'Music';
|
||||
} else if (navigator.userAgent.indexOf('Win') > 0) {
|
||||
return 'Cider';
|
||||
} else {
|
||||
return 'Cider';
|
||||
}
|
||||
}
|
||||
},
|
||||
checkOrientation() {
|
||||
// check orientation of device
|
||||
if (window.innerHeight > window.innerWidth) {
|
||||
return 'portrait'
|
||||
} else {
|
||||
return 'landscape';
|
||||
}
|
||||
},
|
||||
checkPlatformMD() {
|
||||
// check if platfom is desktop or mobile
|
||||
if (navigator.userAgent.match(/Android/i)) {
|
||||
return "mobile";
|
||||
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
|
||||
return "mobile";
|
||||
} else {
|
||||
if (navigator.userAgent.indexOf('Mac') > 0) {
|
||||
return 'desktop';
|
||||
} else if (navigator.userAgent.indexOf('Win') > 0) {
|
||||
return 'desktop';
|
||||
} else {
|
||||
return 'desktop';
|
||||
}
|
||||
}
|
||||
},
|
||||
checkPlatform() {
|
||||
if (navigator.userAgent.match(/Android/i)) {
|
||||
return "android";
|
||||
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
|
||||
return "ios";
|
||||
} else {
|
||||
if (navigator.userAgent.indexOf('Mac') > 0) {
|
||||
return 'mac';
|
||||
} else if (navigator.userAgent.indexOf('Win') > 0) {
|
||||
return 'win';
|
||||
} else {
|
||||
return 'linux';
|
||||
}
|
||||
}
|
||||
},
|
||||
artworkPlaying() {
|
||||
if (this.player.currentMediaItem.status) {
|
||||
return
|
||||
} else {
|
||||
return ["paused"]
|
||||
}
|
||||
},
|
||||
setAutoplay(value) {
|
||||
socket.send(JSON.stringify({
|
||||
"action": "set-autoplay",
|
||||
"autoplay": value
|
||||
}));
|
||||
this.getCurrentMediaItem()
|
||||
if (value) {
|
||||
setTimeout(() => {
|
||||
this.getQueue()
|
||||
}, 1000)
|
||||
} else {
|
||||
this.getQueue()
|
||||
}
|
||||
},
|
||||
seekTo(time, adjust = true) {
|
||||
if (adjust) {
|
||||
time = parseInt(time / 1000)
|
||||
}
|
||||
socket.send(JSON.stringify({
|
||||
action: "seek",
|
||||
time: time
|
||||
}));
|
||||
},
|
||||
setVolume(volume) {
|
||||
socket.send(JSON.stringify({
|
||||
action: "volume",
|
||||
volume: volume
|
||||
}));
|
||||
},
|
||||
getQueue() {
|
||||
socket.send(JSON.stringify({
|
||||
action: "get-queue"
|
||||
}))
|
||||
},
|
||||
play() {
|
||||
socket.send(JSON.stringify({
|
||||
action: "play"
|
||||
}))
|
||||
},
|
||||
pause() {
|
||||
socket.send(JSON.stringify({
|
||||
action: "pause"
|
||||
}))
|
||||
},
|
||||
next() {
|
||||
socket.send(JSON.stringify({
|
||||
action: "next"
|
||||
}))
|
||||
},
|
||||
previous() {
|
||||
socket.send(JSON.stringify({
|
||||
action: "previous"
|
||||
}))
|
||||
},
|
||||
searchArtist() {
|
||||
this.search.query = this.player.currentMediaItem.artistName;
|
||||
this.screen = "search";
|
||||
this.searchQuery();
|
||||
},
|
||||
trackSelect(song) {
|
||||
this.search.selected = song;
|
||||
this.search.trackSelect = true
|
||||
},
|
||||
clearSelectedTrack() {
|
||||
this.search.selected = {}
|
||||
this.search.trackSelect = false
|
||||
},
|
||||
getArtworkColor(hex) {
|
||||
return `#${hex}`
|
||||
},
|
||||
playMediaItemById(id, kind = "song") {
|
||||
socket.send(JSON.stringify({
|
||||
action: "play-mediaitem",
|
||||
id: id,
|
||||
kind: kind
|
||||
}))
|
||||
this.screen = "player";
|
||||
},
|
||||
playNext(type, id) {
|
||||
socket.send(JSON.stringify({
|
||||
action: "play-next",
|
||||
type: type,
|
||||
id: id
|
||||
}))
|
||||
},
|
||||
playLater(type, id) {
|
||||
socket.send(JSON.stringify({
|
||||
action: "play-later",
|
||||
type: type,
|
||||
id: id
|
||||
}))
|
||||
},
|
||||
searchQuery() {
|
||||
if (this.search.query.length == 0) {
|
||||
this.search.state = 0;
|
||||
return;
|
||||
}
|
||||
this.search.state = 1;
|
||||
var actionType = "search"
|
||||
if (this.search.searchType == "library") {
|
||||
actionType = "library-search"
|
||||
}
|
||||
socket.send(JSON.stringify({
|
||||
"action": actionType,
|
||||
"term": this.search.query,
|
||||
"limit": 20
|
||||
}))
|
||||
},
|
||||
quickSearch() {
|
||||
var search = prompt("Search for a song", "")
|
||||
if (search == null || search == "") {
|
||||
return
|
||||
}
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
action: "quick-play",
|
||||
term: search
|
||||
}))
|
||||
},
|
||||
parseTime(value) {
|
||||
var minutes = Math.floor(value / 60000);
|
||||
var seconds = ((value % 60000) / 1000).toFixed(0);
|
||||
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
|
||||
},
|
||||
parseTimeDecimal(value) {
|
||||
var minutes = Math.floor(value / 60000);
|
||||
var seconds = ((value % 60000) / 1000).toFixed(0);
|
||||
return minutes + "." + (seconds < 10 ? '0' : '') + seconds;
|
||||
},
|
||||
hmsToSecondsOnly(str) {
|
||||
var p = str.split(':'),
|
||||
s = 0,
|
||||
m = 1;
|
||||
|
||||
while (p.length > 0) {
|
||||
s += m * parseInt(p.pop(), 10);
|
||||
m *= 60;
|
||||
}
|
||||
|
||||
return s;
|
||||
},
|
||||
getCurrentTime() {
|
||||
return parseFloat(this.hmsToSecondsOnly(this.parseTime(this.player.currentMediaItem.durationInMillis - this.player.currentMediaItem.remainingTime)));
|
||||
},
|
||||
percentage(partial, full) {
|
||||
return (100 * partial) / full
|
||||
},
|
||||
getLyricBGStyle(start, end) {
|
||||
var currentTime = this.getCurrentTime();
|
||||
var duration = this.player.currentMediaItem.durationInMillis
|
||||
var start2 = this.hmsToSecondsOnly(start)
|
||||
var end2 = this.hmsToSecondsOnly(end)
|
||||
var currentProgress = ((100 * (currentTime)) / (end2))
|
||||
// check if currenttime is between start and end
|
||||
this.player.lyricsDebug.start = start2
|
||||
this.player.lyricsDebug.end = end2
|
||||
this.player.lyricsDebug.current = currentTime
|
||||
if (currentTime >= start2 && currentTime <= end2) {
|
||||
return {
|
||||
"--bgSpeed": `${(end2 - start2)}s`
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
getLyricClass(start, end) {
|
||||
var currentTime = this.getCurrentTime();
|
||||
// check if currenttime is between start and end
|
||||
if (currentTime >= start && currentTime <= end) {
|
||||
setTimeout(() => {
|
||||
if (document.querySelector(".lyric-line.active")) {
|
||||
document.querySelector(".lyric-line.active").scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center"
|
||||
})
|
||||
}
|
||||
}, 200)
|
||||
return "active"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
},
|
||||
getAlbumArtUrl(size = 600) {
|
||||
if (this.player.currentMediaItem.artwork) {
|
||||
return `url("${this.player.currentMediaItem.artwork.url.replace('{w}', size).replace('{h}', size)}")`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
getAlbumArtUrlList(url, size = 64) {
|
||||
return `url("${url.replace('{w}', size).replace('{h}', size)}")`;
|
||||
},
|
||||
searchTabClass(tab) {
|
||||
if (tab == this.search.tab) {
|
||||
return "active";
|
||||
}
|
||||
},
|
||||
searchTypeClass(type) {
|
||||
if (type == this.search.searchType) {
|
||||
return "active";
|
||||
}
|
||||
},
|
||||
getQueuePositionClass(position) {
|
||||
if (this.player.queue["_position"] == position) {
|
||||
return ["playing", "passed"]
|
||||
} else if (this.player.queue["_position"] > position) {
|
||||
return ["passed"]
|
||||
}
|
||||
},
|
||||
showSearch(reset = false) {
|
||||
if (reset) {
|
||||
this.search.lastPage = "search"
|
||||
}
|
||||
switch (this.search.lastPage) {
|
||||
case "search":
|
||||
this.screen = "search"
|
||||
break;
|
||||
case "album":
|
||||
this.screen = "album-page"
|
||||
break;
|
||||
case "artist":
|
||||
this.screen = "artist-page"
|
||||
break;
|
||||
case "playlist":
|
||||
this.screen = "playlist-page"
|
||||
break;
|
||||
}
|
||||
},
|
||||
showArtistByName(name) {
|
||||
this.musicKitAPI("search", name, { types: "artists" })
|
||||
},
|
||||
showAlbum(id) {
|
||||
this.search.lastPage = "album"
|
||||
this.screen = "album-page"
|
||||
this.musicKitAPI("album", id, {})
|
||||
},
|
||||
showArtist(id) {
|
||||
this.search.lastPage = "artist"
|
||||
this.screen = "artist-page"
|
||||
this.musicKitAPI("artist", id, { include: "songs,playlists,albums" })
|
||||
},
|
||||
showQueue() {
|
||||
this.queue.temp = this.player["queue"]["_queueItems"]
|
||||
this.screen = "queue"
|
||||
this.getQueue()
|
||||
},
|
||||
queueMove(evt) {
|
||||
console.log(evt)
|
||||
console.log(`new: ${evt.moved.newIndex} old: ${evt.moved.oldIndex}`)
|
||||
this.queue.temp.splice(evt.moved.newIndex, 0, this.queue.temp.splice(evt.moved.oldIndex, 1)[0])
|
||||
socket.send(JSON.stringify({
|
||||
action: "queue-move",
|
||||
from: evt.moved.oldIndex,
|
||||
to: evt.moved.newIndex
|
||||
}))
|
||||
this.getQueue()
|
||||
return true
|
||||
},
|
||||
repeat() {
|
||||
socket.send(JSON.stringify({
|
||||
action: "repeat"
|
||||
}))
|
||||
this.getCurrentMediaItem()
|
||||
},
|
||||
shuffle() {
|
||||
socket.send(JSON.stringify({
|
||||
action: "shuffle"
|
||||
}))
|
||||
this.getCurrentMediaItem()
|
||||
},
|
||||
setShuffle(val) {
|
||||
socket.send(JSON.stringify({
|
||||
action: "set-shuffle",
|
||||
shuffle: val
|
||||
}))
|
||||
this.getCurrentMediaItem()
|
||||
},
|
||||
getMediaPalette(data) {
|
||||
var palette = {
|
||||
'--bgColor': `#${data['artwork']['bgColor']}`,
|
||||
'--textColor1': `#${data['artwork']['textColor1']}`,
|
||||
'--textColor2': `#${data['artwork']['textColor2']}`,
|
||||
'--textColor3': `#${data['artwork']['textColor3']}`,
|
||||
'--textColor4': `#${data['artwork']['textColor4']}`
|
||||
}
|
||||
return palette
|
||||
},
|
||||
playAlbum(id, shuffle = false) {
|
||||
if (shuffle) {
|
||||
this.setShuffle(true)
|
||||
} else {
|
||||
this.setShuffle(false)
|
||||
}
|
||||
this.playMediaItemById(id, 'album');
|
||||
},
|
||||
getLyrics() {
|
||||
socket.send(JSON.stringify({
|
||||
action: "get-lyrics",
|
||||
}))
|
||||
},
|
||||
showLyrics() {
|
||||
this.getLyrics()
|
||||
this.screen = "lyrics"
|
||||
},
|
||||
showLyricsInline() {
|
||||
this.getLyrics()
|
||||
this.player.lowerPanelState = "lyrics"
|
||||
},
|
||||
parseLyrics() {
|
||||
var xml = this.stringToXml(this.player.lyricsMediaItem.ttml)
|
||||
var json = xmlToJson(xml);
|
||||
this.player.lyrics = json
|
||||
},
|
||||
stringToXml(st) {
|
||||
// string to xml
|
||||
var xml = (new DOMParser()).parseFromString(st, "text/xml");
|
||||
return xml;
|
||||
},
|
||||
canShowSearchTab(tab) {
|
||||
if (tab == this.search.tab || this.search.tab == "all") {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getCurrentMediaItem() {
|
||||
socket.send(JSON.stringify({
|
||||
action: "get-currentmediaitem"
|
||||
}))
|
||||
},
|
||||
setStreamerOverlay() {
|
||||
document.body.classList.add("streamer-overlay")
|
||||
},
|
||||
setMode(mode) {
|
||||
switch (mode) {
|
||||
default: this.screen = "player"
|
||||
break;
|
||||
case "miniplayer":
|
||||
this.screen = "miniplayer"
|
||||
break;
|
||||
}
|
||||
},
|
||||
connect() {
|
||||
let self = this;
|
||||
this.connectedState = 0;
|
||||
if (this.url === "") {
|
||||
this.url = prompt("Host IP", "localhost")
|
||||
}
|
||||
socket = new WebSocket(`ws://${this.url}:26369`);
|
||||
socket.onopen = (e) => {
|
||||
console.log(e);
|
||||
console.log('connected');
|
||||
app.connectedState = 1;
|
||||
if (getParameterByName("mode")) {
|
||||
self.setMode(getParameterByName("mode"))
|
||||
} else {
|
||||
self.setMode("default")
|
||||
}
|
||||
self.clearSelectedTrack()
|
||||
}
|
||||
|
||||
socket.onclose = (e) => {
|
||||
console.log(e);
|
||||
console.log('disconnected');
|
||||
app.connectedState = 2;
|
||||
}
|
||||
|
||||
socket.onerror = (e) => {
|
||||
console.log(e);
|
||||
console.log('error');
|
||||
app.connectedState = 2;
|
||||
}
|
||||
|
||||
socket.onmessage = (e) => {
|
||||
console.log(e.data)
|
||||
const response = JSON.parse(e.data);
|
||||
switch (response.type) {
|
||||
default: console.log(response);
|
||||
break;
|
||||
case "musickitapi.search":
|
||||
self.showArtist(response.data["artists"][0]["id"]);
|
||||
break;
|
||||
case "musickitapi.album":
|
||||
if (self.screen == "album-page") {
|
||||
self.albumPage.data = response.data
|
||||
}
|
||||
break;
|
||||
case "musickitapi.artist":
|
||||
if (self.screen == "artist-page") {
|
||||
self.artistPage.data = response.data
|
||||
}
|
||||
break;
|
||||
case "queue":
|
||||
self.player.queue = response.data;
|
||||
self.queue.temp = response.data["_queueItems"];
|
||||
self.$forceUpdate()
|
||||
break;
|
||||
case "lyrics":
|
||||
self.player.lyrics = response.data;
|
||||
self.$forceUpdate()
|
||||
break;
|
||||
case "searchResultsLibrary":
|
||||
self.search.results = response.data;
|
||||
self.search.state = 2;
|
||||
break;
|
||||
case "searchResults":
|
||||
self.search.results = response.data;
|
||||
self.search.state = 2;
|
||||
break;
|
||||
case "playbackStateUpdate":
|
||||
if (!self.player.userInteraction) {
|
||||
self.updatePlaybackState(response.data)
|
||||
}
|
||||
break;
|
||||
}
|
||||
// console.log(e.data);
|
||||
}
|
||||
},
|
||||
updatePlaybackState(mediaitem) {
|
||||
var lyricsDisplayed = this.screen == "lyrics" || this.player.lowerPanelState == "lyrics"
|
||||
if (this.player.currentMediaItem["isrc"] != mediaitem["isrc"]) {
|
||||
if (lyricsDisplayed) {
|
||||
this.getLyrics()
|
||||
}
|
||||
if (this.screen == "queue") {
|
||||
this.getQueue()
|
||||
}
|
||||
}
|
||||
this.player.currentMediaItem = mediaitem
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, '\\$&');
|
||||
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
function xmlToJson(xml) {
|
||||
|
||||
// Create the return object
|
||||
var obj = {};
|
||||
|
||||
if (xml.nodeType == 1) { // element
|
||||
// do attributes
|
||||
if (xml.attributes.length > 0) {
|
||||
obj["@attributes"] = {};
|
||||
for (var j = 0; j < xml.attributes.length; j++) {
|
||||
var attribute = xml.attributes.item(j);
|
||||
obj["@attributes"][attribute.nodeName] = attribute.nodeValue;
|
||||
}
|
||||
}
|
||||
} else if (xml.nodeType == 3) { // text
|
||||
obj = xml.nodeValue;
|
||||
}
|
||||
|
||||
// do children
|
||||
if (xml.hasChildNodes()) {
|
||||
for (var i = 0; i < xml.childNodes.length; i++) {
|
||||
var item = xml.childNodes.item(i);
|
||||
var nodeName = item.nodeName;
|
||||
if (typeof(obj[nodeName]) == "undefined") {
|
||||
obj[nodeName] = xmlToJson(item);
|
||||
} else {
|
||||
if (typeof(obj[nodeName].push) == "undefined") {
|
||||
var old = obj[nodeName];
|
||||
obj[nodeName] = [];
|
||||
obj[nodeName].push(old);
|
||||
}
|
||||
obj[nodeName].push(xmlToJson(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
window.onresize = function() {
|
||||
app.resetPlayerUI()
|
||||
}
|
||||
|
||||
app.connect()
|
42
src/web-remote/manifest.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"name": "Cider Remote",
|
||||
"short_name": "Cider Remote",
|
||||
"description": "Cider Remote",
|
||||
"developer": {
|
||||
"name": "Cider Collective",
|
||||
"url": "https://cider.sh?utm-source=manifest"
|
||||
},
|
||||
"homepage_url": "https://cider.sh?utm-source=manifest",
|
||||
"icons": [{
|
||||
"src": "/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icon-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"protocol_handlers": [{
|
||||
"protocol": "ext+cider",
|
||||
"name": "Cider",
|
||||
"uriTemplate": "/?url=%s"
|
||||
}]
|
||||
|
||||
}
|
2
src/web-remote/sortable.min.js
vendored
Normal file
1023
src/web-remote/style.css
Normal file
6
src/web-remote/vue.js
Normal file
2
src/web-remote/vuedraggable.umd.min.js
vendored
Normal file
18
tsconfig.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./build",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["node_modules/*"]
|
||||
},
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|