Merged main branch and updates win.ts with some dumb stuff that still hasn't fixed it

This commit is contained in:
Core 2022-01-07 20:47:34 +00:00
parent 8e1d2dc96b
commit 57c7002b25
No known key found for this signature in database
GPG key ID: 1B77805746C47C28
59 changed files with 16275 additions and 1626 deletions

View file

@ -32,11 +32,13 @@
"electron-store": "^8.0.1",
"electron-updater": "^4.6.1",
"electron-window-state": "^5.0.3",
"es6-promise": "^4.2.8",
"express": "^4.17.2",
"get-port": "^5.1.1",
"lastfmapi": "^0.1.1",
"mpris-service": "^2.1.2",
"music-metadata": "^7.11.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"source-map-support": "^0.5.21",
"v8-compile-cache": "^2.3.0",
"ws": "^8.4.0",
@ -76,9 +78,9 @@
}
],
"build": {
"electronVersion": "15.3.4",
"electronVersion": "16.0.6",
"electronDownload": {
"version": "15.3.4-wvvmp",
"version": "16.0.6+wvcus",
"mirror": "https://github.com/castlabs/electron-releases/releases/download/v"
},
"appId": "cider",
@ -102,7 +104,10 @@
],
"linux": {
"target": [
"AppImage"
"AppImage",
"deb",
"snap",
"rpm"
],
"synopsis": "A new look into listening and enjoying music in style and performance. ",
"category": "AudioVideo",

View file

@ -1,407 +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 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.ico`),
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
if (process.platform === "darwin" || process.platform === "linux") {
win = new BrowserWindow(options)
} else {
if (app.cfg.get("visual.window_transparency") !== "disabled") {
const { BrowserWindow } = require("electron-acrylic-window");
}
win = new BrowserWindow(options)
win.setVibrancy("dark")
}
// 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 })
})
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();
})
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)
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.updateState(a)
lastfm.scrobbleSong(a)
lastfm.updateNowPlayingSong(a)
});
ipcMain.on('nowPlayingItemDidChange', (_event, a) => {
app.media = a;
discord.updateActivity(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;
console.log("lfmtoken", String(startArgs))
if (String(startArgs).includes('auth')) {
let authURI = String(startArgs).split('/auth/')[1]
if (authURI.startsWith('lastfm')) { // If we wanted more auth options
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 {
const formattedSongID = startArgs.replace('ame://', '').replace('/', '');
console.warn(`[LinkHandler] Attempting to load song id: ${formattedSongID}`);
// setQueue can be done with album, song, url, playlist id
this.win.webContents.executeJavaScript(`
MusicKit.getInstance().setQueue({ song: '${formattedSongID}'}).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");
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();
}
});
webapp.use(express.static(webRemotePath));
webapp.get('/', function (req, res) {
//res.sendFile(path.join(webRemotePath, 'index_old.html'));
res.render("main", CiderBase.EnvironmentVariables)
});
webapp.get('/audio.webm', function (req, res) {
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 client port: ${CiderBase.clientPort}`);
});
},
}
module.exports = CiderBase;

View file

@ -1,17 +1,25 @@
import * as path from "path";
import * as electron from "electron";
import * as electronAcrylic from "electron-acrylic-window"
// import * as electronAcrylic from "electron-acrylic-window"
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";
export class Win {
win: any | undefined;
win: any | undefined = null;
app: electron.App | undefined;
private srcPath: string = path.join(__dirname, "../../src");
private resourcePath: string = path.join(__dirname, "../../resources");
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 EnvironmentVariables: object = {
"env": {
@ -20,16 +28,17 @@ export class Win {
}
};
private options: any = {
icon: path.join(this.resourcePath, `icons/icon.ico`),
icon: path.join(this.paths.resourcePath, `icons/icon.` + (process.platform === "win32" ? "ico" : "png")),
width: 1024,
height: 600,
x: undefined,
y: undefined,
minWidth: 850,
minHeight: 400,
minWidth: 844,
minHeight: 410,
frame: false,
title: "Cider",
transparent: process.platform === "darwin",
vibrancy: 'dark',
// transparent: true,
hasShadow: false,
webPreferences: {
webviewTag: true,
@ -42,16 +51,14 @@ export class Win {
sandbox: true,
nativeWindowOpen: true,
contextIsolation: false,
preload: path.join(this.srcPath, 'preload/cider-preload.js')
preload: path.join(this.paths.srcPath, './preload/cider-preload.js')
}
};
/**
* Creates the browser window
*/
async createWindow(): Promise<any | undefined> {
this.clientPort = await getPort({port: 9000});
createWindow(): void {
// Load the previous state with fallback to defaults
const windowState = windowStateKeeper({
defaultWidth: 1024,
@ -60,61 +67,75 @@ export class Win {
this.options.width = windowState.width;
this.options.height = windowState.height;
this.startWebServer()
// Start the webserver for the browser window to load
this.startWebServer().then(() => {
if (process.platform === "win32") {
this.win = new electronAcrylic.BrowserWindow(this.options);
// this.win = new electronAcrylic.BrowserWindow(this.options);
} else {
this.win = new electron.BrowserWindow(this.options);
}
// Create the browser window.
console.debug('Browser window created');
})
// and load the renderer.
this.startSession(this.win);
this.startHandlers(this.win);
// Register listeners on Window to track size and position of the Window.
windowState.manage(this.win);
return this.win;
}
/**
* Starts the webserver for the renderer process.
*/
private startWebServer(): void {
const webapp = express();
const webRemotePath = path.join(this.srcPath, 'renderer');
webapp.set("views", path.join(webRemotePath, "views"));
webapp.set("view engine", "ejs");
public async startWebServer(): Promise<void> {
this.clientPort = await getPort({port: 9000});
const app = express();
webapp.use(function (req, res, next) {
// if not localhost
// @ts-ignore
if (req.url.includes("audio.webm") || (req.headers.host.includes("localhost") && req.headers["user-agent"].includes("Cider"))) {
next();
}
});
// app.use(express.static(path.join(this.paths.srcPath, './renderer/'))); // this breaks everything
app.set("views", path.join(this.paths.srcPath, './renderer/views'));
app.set("view engine", "ejs");
webapp.use(express.static(webRemotePath));
webapp.get('/', (req, res) => {
//res.sendFile(path.join(webRemotePath, 'index_old.html'));
console.log(req)
// this is also causing issues
// app.use((req, res, next) => {
// // if not localhost
//
// // @ts-ignore
// if (req.url.includes("audio.webm") || (req.headers.host.includes("localhost") && req.headers["user-agent"].includes("Cider"))) {
// next();
// }
// });
app.get('/', (req, res) => {
// res.send("Hello world!");
// res.sendFile(path.join(webRemotePath, 'index_old.html'));
res.render("main", this.EnvironmentVariables)
});
// webelectron.app.get('/audio.webm', (req, res) => {
// app.get('/audio.webm', (req, res) => {
// try {
// req.connection.setTimeout(Number.MAX_SAFE_INTEGER);
// this.audiostream.on('data', (data) => {
// // 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) }
// } catch (ex) {
// console.log(ex)
// }
// });
webapp.listen(this.clientPort, () => {
app.listen(this.clientPort, () => {
console.log(`Cider client port: ${this.clientPort}`);
});
}
@ -122,8 +143,7 @@ export class Win {
/**
* Starts the session for the renderer process.
*/
private startSession(win: any): void {const self = this;
private startSession(win: any): 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
win.webContents.session.webRequest.onBeforeRequest(
{
@ -132,7 +152,7 @@ export class Win {
(details: { url: string | string[]; }, callback: (arg0: { redirectURL?: string; cancel?: boolean; }) => void) => {
if (details.url.includes("hls.js")) {
callback({
redirectURL: `http://localhost:${self.clientPort}/apple-hls.js`
redirectURL: `http://localhost:${this.clientPort}/apple-hls.js`
})
} else {
callback({
@ -146,20 +166,15 @@ export class Win {
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}`
let itspod = await win.webContents.executeJavaScript(`window.localStorage.getItem("music.ampwebplay.itspod")`)
if (itspod != null)
details.requestHeaders['Cookie'] = `itspod=${itspod}`
}
callback({requestHeaders: details.requestHeaders})
})
const location = `http://localhost:${this.clientPort}/`
console.log('yeah')
let location = `http://localhost:${this.clientPort}/`
win.loadURL(location)
.then(() => {
console.debug(`Cider client location: ${location}`);
})
.catch(console.error);
}
/**
@ -167,6 +182,116 @@ export class Win {
* @param win The BrowserWindow
*/
private startHandlers(win: any): 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 = !electron.app.isPackaged
})
electron.ipcMain.on('close', () => { // listen for close event
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('getStoreValue', (event, key, defaultValue) => {
// return (defaultValue ? app.cfg.get(key, true) : app.cfg.get(key));
// });
//
// electron.ipcMain.handle('setStoreValue', (event, key, value) => {
// app.cfg.set(key, value);
// });
//
// electron.ipcMain.on('getStore', (event) => {
// event.returnValue = app.cfg.store
// })
//
// electron.ipcMain.on('setStore', (event, store) => {
// app.cfg.store = store
// })
electron.ipcMain.handle('setVibrancy', (event, key, value) => {
win.setVibrancy(value)
});
electron.ipcMain.on('maximize', () => { // listen for maximize event
if (win.isMaximized()) {
win.unmaximize()
} else {
win.maximize()
}
})
electron.ipcMain.on('minimize', () => { // listen for minimize event
win.minimize();
})
// Set scale
electron.ipcMain.on('setScreenScale', (event, scale) => {
win.webContents.setZoomFactor(parseFloat(scale))
})
/* *********************************************************************************************
* Window Events
* **********************************************************************************************/
if (process.platform === "win32") {
let WND_STATE = {
MINIMIZED: 0,
@ -187,83 +312,26 @@ export class Win {
wndState = WND_STATE.FULL_SCREEN
} else if (isMaximized && state !== WND_STATE.MAXIMIZED) {
wndState = WND_STATE.MAXIMIZED
win.webContents.executeJavaScript(`electron.app.chrome.maximized = true`)
win.webContents.executeJavaScript(`app.chrome.maximized = true`)
} else if (state !== WND_STATE.NORMAL) {
wndState = WND_STATE.NORMAL
win.webContents.executeJavaScript(`electron.app.chrome.maximized = false`)
win.webContents.executeJavaScript(`app.chrome.maximized = false`)
}
})
}
win.on("closed", () => {
this.win = null
})
// Set window Handler
win.webContents.setWindowOpenHandler((x: any) => {
if (x.url.includes("apple") || x.url.includes("localhost")) {
return {action: "allow"}
}
electron.shell.openExternal(x.url).catch(() => {
})
return {
action: 'deny'
}
electron.shell.openExternal(x.url).catch(console.error)
return {action: 'deny'}
})
//-------------------------------------------------------------------------------
// Renderer IPC Listeners
//-------------------------------------------------------------------------------
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 = !electron.app.isPackaged
})
// IPC stuff (listeners)
electron.ipcMain.on('close', () => { // listen for close event
win.close();
})
electron.ipcMain.handle('getYTLyrics', async (event, track, artist) => {
const u = track + " " + artist + " official video";
return await yt.search(u)
})
// electron.ipcMain.handle('getStoreValue', (event, key, defaultValue) => {
// return (defaultValue ? electron.app.cfg.get(key, true) : electron.app.cfg.get(key));
// });
//
// electron.ipcMain.handle('setStoreValue', (event, key, value) => {
// electron.app.cfg.set(key, value);
// });
//
// electron.ipcMain.on('getStore', (event) => {
// event.returnValue = electron.app.cfg.store
// })
//
// electron.ipcMain.on('setStore', (event, store) => {
// electron.app.cfg.store = store
// })
electron.ipcMain.on('maximize', () => { // listen for maximize event
if (win.isMaximized()) {
win.unmaximize()
} else {
win.maximize()
}
})
electron.ipcMain.on('minimize', () => { // listen for minimize event
win.minimize();
})
// Set scale
electron.ipcMain.on('setScreenScale', (event, scale) => {
win.webContents.setZoomFactor(parseFloat(scale))
})
}
}

View file

@ -1,90 +0,0 @@
const lastfm = require('./plugins/lastfm');
const win = require('./base/win')
// Analytics for debugging.
const ElectronSentry = require("@sentry/electron");
const {app} = require("electron");
ElectronSentry.init({dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214"});
module.exports = {
//-------------------------------------------------------------------------------
// Public Methods
//-------------------------------------------------------------------------------
/**
* Starts the application (called on on-ready). - Starts BrowserWindow and WebServer
*/
Start() {
app.win = win.createBrowserWindow()
},
/**
* Initializes the main application (run before on-ready)
*/
async Init() {
// Initialize the config.
const {init} = require("./base/store");
await init()
//-------------------------------------------------------------------------------
// Append Commandline Arguments
//-------------------------------------------------------------------------------
// Hardware Acceleration
// Enable WebGPU and list adapters (EXPERIMENTAL.)
// Note: THIS HAS TO BE BEFORE ANYTHING GETS INITIALIZED.
switch (app.cfg.get("visual.hw_acceleration")) {
default:
case "default":
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;
}
if (process.platform === "linux") {
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
}
app.commandLine.appendSwitch('high-dpi-support', 'true');
app.commandLine.appendSwitch('force-device-scale-factor', '1');
app.commandLine.appendSwitch('disable-pinch');
app.commandLine.appendSwitch('js-flags', '--max-old-space-size=1024')
},
/**
* Handles all links being opened in the application.
*/
LinkHandler(startArgs) {
if (!startArgs) return;
console.log("lfmtoken", String(startArgs))
if (String(startArgs).includes('auth')) {
let authURI = String(startArgs).split('/auth/')[1]
if (authURI.startsWith('lastfm')) { // If we wanted more auth options
const authKey = authURI.split('lastfm?token=')[1];
app.cfg.set('lastfm.enabled', true);
app.cfg.set('lastfm.auth_token', authKey);
app.win.webContents.send('LastfmAuthenticated', authKey);
lastfm.authenticate()
}
} else {
const formattedSongID = startArgs.replace('ame://', '').replace('/', '');
console.warn(`[LinkHandler] Attempting to load song id: ${formattedSongID}`);
// setQueue can be done with album, song, url, playlist id
this.win.webContents.executeJavaScript(`
MusicKit.getInstance().setQueue({ song: '${formattedSongID}'}).then(function(queue) {
MusicKit.getInstance().play();
});
`).catch((err) => console.error(err));
}
},
}

View file

@ -16,7 +16,7 @@ import {Win} from "./base/win";
const Cider = new Win()
app.on("ready", () => {
Cider.createWindow();
Cider.startWebServer();
});
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -1,138 +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) 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})`);
if (app.discord.activityCache) {
client.setActivity(app.discord.activityCache).catch((e) => console.error(e));
app.discord.activityCache = null;
}
})
// 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) {
this.connect()
}
if (!app.discord.isConnected) return;
// console.log('[DiscordRPC][updateActivity] Updating Discord Activity.')
const listenURL = `https://applemusicelectron.com/p?id=${attributes.playParams.id}`
//console.log(attributes)
let ActivityObject = {
details: attributes.name,
state: `by ${attributes.artistName}`,
startTimestamp: attributes.startTime,
endTimestamp: attributes.endTime,
largeImageKey: (attributes.artwork.url.replace('{w}', '512').replace('{h}', '512')) ?? '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"
}
// console.log(`[LinkHandler] Listening URL has been set to: ${listenURL}`);
if (app.cfg.get('general.discordClearActivityOnPause') == 1) {
delete ActivityObject.smallImageKey
delete ActivityObject.smallImageText
}
// Check all the values work
if (!((new Date(attributes.endTime)).getTime() > 0)) {
delete ActivityObject.startTimestamp
delete ActivityObject.endTimestamp
}
if (!attributes.artistName) {
delete ActivityObject.state
}
if (!ActivityObject.largeImageText || ActivityObject.largeImageText.length < 2) {
delete ActivityObject.largeImageText
}
if (ActivityObject.details.length > 128) {
AcitivityObject.details = ActivityObject.details.substring(0, 125) + '...'
}
// Clear if if needed
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) {
try {
// console.log(`[DiscordRPC][setActivity] Setting activity to ${JSON.stringify(ActivityObject)}`);
app.discord.setActivity(ActivityObject)
} catch (err) {
console.error(`[DiscordRPC][setActivity] ${err}`)
}
}
},
}

View file

@ -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, app.cfg.get('lastfm.scrobble_after') * 1000));
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;

View file

@ -1,113 +0,0 @@
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;
}
},
/**
* Closes the MPRIS interface.
*/
clearActivity: () => {
if (process.platform !== "linux") return;
mediaPlayer.metadata = {'mpris:trackid': '/org/mpris/MediaPlayer2/TrackList/NoTrack'}
mediaPlayer.playbackStatus = 'Stopped';
},
}

View file

@ -98,59 +98,7 @@ input[type=range].md-slider::-webkit-slider-runnable-track {
}
.md-close-btn {
-webkit-mask-image: url("ameres://icons/webui/close.svg");
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
background-color: white;
opacity: 0.75;
-webkit-mask-size: contain;
height: 18px;
width: 18px;
}
.md-btn {
background: rgba(100, 100, 100, 0.5);
padding: 8px 16px;
border-radius: 4px;
font-size: 15px;
border: 1px solid rgb(100 100 100 / 35%);
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.3), 0px 1px 1px rgba(0, 0, 0, 0.4);
}
.md-btn.md-btn-block {
display: block;
}
.md-btn:active {
filter: brightness(75%);
}
.md-select {
padding: 5px 10px;
font-size: 1em;
font-family: inherit;
border-radius: 4px;
border: 1px solid rgb(100 100 100 / 35%);
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.3), 0px 1px 1px rgba(0, 0, 0, 0.4);
background: #363636;
color: #eee;
}
.md-select:focus {
outline: none;
}
.md-select > option {
font-size: 1em;
font-family: inherit;
padding: 8px 16px;
}
.md-btn.md-btn-primary {
background: var(--keyColor);
color: white;
}
.md-transparent {
background: transparent;
@ -179,7 +127,6 @@ input[type=range].md-slider::-webkit-slider-runnable-track {
@media (prefers-color-scheme: light) {
.md-btn {
background: rgb(255 255 255);
box-shadow: rgb(0 0 0 / 10%) 0px 0px 1px, rgb(0 0 0 / 20%) 0px 1px 1px;
border: 1px solid rgb(0 0 0 / 15%);
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-disc"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle></svg>

After

Width:  |  Height:  |  Size: 295 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>

After

Width:  |  Height:  |  Size: 311 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>

After

Width:  |  Height:  |  Size: 409 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>

After

Width:  |  Height:  |  Size: 371 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>

After

Width:  |  Height:  |  Size: 332 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-list" style="width: min-content;" ><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 511 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-music"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>

After

Width:  |  Height:  |  Size: 327 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-play-circle"><circle cx="12" cy="12" r="10"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon></svg>

After

Width:  |  Height:  |  Size: 313 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-play"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>

After

Width:  |  Height:  |  Size: 263 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 351 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 304 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-radio"><circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path></svg>

After

Width:  |  Height:  |  Size: 389 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-share-2"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>

After

Width:  |  Height:  |  Size: 445 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-share"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg>

After

Width:  |  Height:  |  Size: 364 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-down"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>

After

Width:  |  Height:  |  Size: 374 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>

After

Width:  |  Height:  |  Size: 348 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>

After

Width:  |  Height:  |  Size: 313 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-circle"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View file

@ -0,0 +1,99 @@
var CiderAudio = {
context : null,
source : null,
audioNodes : {
gainNode : null,
spatialNode : null,
spatialInput: null,
},
init: function (cb = function () { }) {
//AudioOutputs.fInit = true;
searchInt = setInterval(function () {
if (document.getElementById("apple-music-player")) {
//AudioOutputs.eqReady = true;
document.getElementById("apple-music-player").crossOrigin = "anonymous";
CiderAudio.connectContext(document.getElementById("apple-music-player"), 0);
cb();
clearInterval(searchInt);
}
}, 1000);
},
off: function(){
try{
CiderAudio.audioNodes.gainNode.disconnect();
CiderAudio.audioNodes.spatialNode.disconnect();
CiderAudio.source.connect(CiderAudio.context.destination);} catch(e){}
},
connectContext: function (mediaElem){
if (!CiderAudio.context){
CiderAudio.context = new (window.AudioContext || window.webkitAudioContext);
}
if (!CiderAudio.source){
CiderAudio.source = CiderAudio.context.createMediaElementSource(mediaElem);
} else {try{CiderAudio.source.disconnect(CiderAudio.context.destination)}catch(e){}}
CiderAudio.audioNodes.gainNode = CiderAudio.context.createGain()
CiderAudio.source.connect(CiderAudio.audioNodes.gainNode);
CiderAudio.audioNodes.gainNode.connect(CiderAudio.context.destination);
if(app.cfg.audio.normalization){
CiderAudio.normalizerOn()
}
if (app.cfg.audio.spatial){
CiderAudio.spatialOn()
}
},
normalizerOn: function (){},
normalizerOff: function (){
CiderAudio.audioNodes.gainNode.gain.setTargetAtTime(1, CiderAudio.context.currentTime+ 1, 0.5);
},
spatialOn: function (){
try{
CiderAudio.audioNodes.gainNode.connect(CiderAudio.context.destination);} catch(e){}
CiderAudio.audioNodes.spatialNode = new ResonanceAudio(CiderAudio.context);
CiderAudio.audioNodes.spatialNode.output.connect(CiderAudio.context.destination);
let roomDimensions = {
width: 32,
height: 12,
depth: 32,
};
let roomMaterials = {
// Room wall materials
left: 'metal',
right: 'metal',
front: 'brick-bare',
back: 'brick-bare',
down: 'acoustic-ceiling-tiles',
up: 'acoustic-ceiling-tiles',
};
CiderAudio.audioNodes.spatialNode.setRoomProperties(roomDimensions, roomMaterials);
CiderAudio.audioNodes.spatialInput = CiderAudio.audioNodes.spatialNode.createSource();
CiderAudio.audioNodes.gainNode.connect(CiderAudio.audioNodes.spatialInput.input);
},
spatialOff: function (){
try{
CiderAudio.audioNodes.spatialNode.output.disconnect(CiderAudio.context.destination);
CiderAudio.audioNodes.gainNode.disconnect(CiderAudio.audioNodes.spatialInput.input);} catch(e){}
CiderAudio.audioNodes.gainNode.connect(CiderAudio.context.destination);
},
sendAudio: function (){
var options = {
mimeType : 'audio/webm; codecs=opus'
};
var destnode = CiderAudio.context.createMediaStreamDestination();
CiderAudio.audioNodes.gainNode.connect(destnode)
var mediaRecorder = new MediaRecorder(destnode.stream,options);
mediaRecorder.start(1);
mediaRecorder.ondataavailable = function(e) {
e.data.arrayBuffer().then(buffer => {
ipcRenderer.send('writeAudio',buffer)
}
);
}
}
}
if (app.cfg.advanced.AudioContext){
CiderAudio.init()
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
<?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 313 105" 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(3.125,0,0,3.125,0,0)">
<g transform="matrix(1.38696,0,0,1.38696,-183.553,-76.7485)">
<g transform="matrix(16,0,0,16,160.665,73.4177)">
<path d="M0.705,-0.473C0.685,-0.641 0.558,-0.737 0.389,-0.737C0.197,-0.737 0.05,-0.602 0.05,-0.364C0.05,-0.126 0.195,0.01 0.389,0.01C0.576,0.01 0.688,-0.114 0.705,-0.248L0.549,-0.249C0.535,-0.171 0.474,-0.126 0.392,-0.126C0.281,-0.126 0.206,-0.208 0.206,-0.364C0.206,-0.515 0.28,-0.601 0.393,-0.601C0.477,-0.601 0.538,-0.553 0.549,-0.473L0.705,-0.473Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(16,0,0,16,172.699,73.4177)">
<path d="M0.06,-0L0.212,-0L0.212,-0.545L0.06,-0.545L0.06,-0ZM0.136,-0.616C0.181,-0.616 0.218,-0.65 0.218,-0.692C0.218,-0.734 0.181,-0.769 0.136,-0.769C0.092,-0.769 0.055,-0.734 0.055,-0.692C0.055,-0.65 0.092,-0.616 0.136,-0.616Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(16,0,0,16,177.051,73.4177)">
<path d="M0.261,0.009C0.349,0.009 0.395,-0.042 0.416,-0.087L0.423,-0.087L0.423,-0L0.572,-0L0.572,-0.727L0.421,-0.727L0.421,-0.454L0.416,-0.454C0.396,-0.498 0.352,-0.553 0.261,-0.553C0.141,-0.553 0.04,-0.46 0.04,-0.272C0.04,-0.089 0.137,0.009 0.261,0.009ZM0.309,-0.112C0.235,-0.112 0.195,-0.178 0.195,-0.273C0.195,-0.367 0.234,-0.432 0.309,-0.432C0.383,-0.432 0.424,-0.37 0.424,-0.273C0.424,-0.175 0.382,-0.112 0.309,-0.112Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(16,0,0,16,187.216,73.4177)">
<path d="M0.309,0.011C0.444,0.011 0.535,-0.055 0.556,-0.156L0.416,-0.165C0.401,-0.124 0.362,-0.102 0.311,-0.102C0.236,-0.102 0.188,-0.152 0.188,-0.234L0.188,-0.234L0.559,-0.234L0.559,-0.276C0.559,-0.461 0.447,-0.553 0.303,-0.553C0.142,-0.553 0.038,-0.439 0.038,-0.27C0.038,-0.097 0.141,0.011 0.309,0.011ZM0.188,-0.328C0.191,-0.39 0.238,-0.44 0.305,-0.44C0.371,-0.44 0.417,-0.393 0.417,-0.328L0.188,-0.328Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(16,0,0,16,196.778,73.4177)">
<path d="M0.06,-0L0.212,-0L0.212,-0.309C0.212,-0.376 0.261,-0.422 0.327,-0.422C0.348,-0.422 0.377,-0.418 0.391,-0.414L0.391,-0.548C0.378,-0.551 0.359,-0.553 0.344,-0.553C0.283,-0.553 0.233,-0.518 0.213,-0.45L0.207,-0.45L0.207,-0.545L0.06,-0.545L0.06,-0Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
<circle cx="17" cy="16.667" r="12.333" style="fill:rgb(235,235,235);"/>
<g id="g147" transform="matrix(0.297476,0,0,0.297476,-16.7212,-19.0819)">
<path id="path41" d="M113.553,67.234C111.989,67.44 110.373,67.38 108.796,67.538C105.581,67.86 102.374,68.548 99.283,69.479C85.683,73.576 74.237,83.188 67.62,95.728C58.974,112.114 59.92,132.602 69.683,148.275C73.899,155.044 79.648,160.705 86.34,165.013C90.987,168.005 96.247,170.235 101.606,171.575C107.932,173.157 114.607,173.607 121.076,172.802C139.182,170.549 155.227,158.921 162.858,142.301C165.794,135.905 167.337,128.957 167.644,121.946C168.67,98.52 152.877,76.772 130.7,69.694C126.785,68.444 122.737,67.681 118.642,67.399C116.992,67.285 115.202,67.018 113.553,67.234M112.226,83.813C116.656,83.29 121.632,84.115 125.833,85.514C130.752,87.152 135.254,89.717 139.108,93.203C153.094,105.857 154.806,128.026 143.09,142.744C140.415,146.104 137.152,149.006 133.466,151.217C128.993,153.901 123.948,155.665 118.753,156.221C113.863,156.745 108.876,156.366 104.15,154.943C99.89,153.66 95.81,151.611 92.313,148.85C77.992,137.54 73.888,116.673 83.621,101.038C86.308,96.722 89.827,93.066 93.973,90.133C99.448,86.26 105.67,84.586 112.226,83.813M112.226,89.802C106.903,90.501 101.862,91.851 97.402,95.001C93.9,97.475 90.841,100.562 88.619,104.246C80.752,117.287 83.774,134.719 95.853,144.135C98.477,146.18 101.448,147.812 104.593,148.901C109.23,150.505 114.244,150.887 119.085,150.235C123.337,149.662 127.573,148.069 131.143,145.694C135.459,142.822 139.035,139.018 141.507,134.447C148.532,121.459 144.666,104.627 132.913,95.798C129.999,93.609 126.685,91.926 123.178,90.925C119.742,89.945 115.8,89.332 112.226,89.802M107.138,109.717C108.23,109.447 110.211,111.095 111.12,111.636C114.483,113.639 117.898,115.564 121.297,117.505C122.215,118.029 124.992,119.047 124.931,120.287C124.871,121.486 122.008,122.553 121.076,123.084C117.931,124.873 114.817,126.715 111.673,128.504C110.475,129.186 108.937,130.499 107.58,130.751C105.799,131.083 106.363,127.602 106.363,126.593L106.363,113.76C106.363,112.776 105.865,110.031 107.138,109.717Z" style="fill:rgb(255,91,109);fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

29
src/renderer/logotmp.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because it is too large Load diff

12492
src/renderer/vibrant.min.js vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,30 +1,139 @@
<script type="text/x-template" id="fullscreen-view">
<div class="fullscreen-view">
<div class="background">
<div class="bgArtworkMaterial">
<div class="bg-artwork-container">
<img v-if="(app.cfg.visual.bg_artwork_rotation || app.animateBackground)" class="bg-artwork a" :src="image.replace('600x600','30x30') ?? ''">
<img v-if="(app.cfg.visual.bg_artwork_rotation || app.animateBackground)" class="bg-artwork b" :src="image.replace('600x600','30x30') ?? ''">
<img v-if="!(app.cfg.visual.bg_artwork_rotation || app.animateBackground)" class="bg-artwork no-animation" :src="image.replace('600x600','30x30') ?? ''">
</div>
</div>
</div>
<div class="row fs-row">
<div class="col artwork-col">
<div class="artwork">
<div class="artwork" @click="app.appMode = 'player'">
<mediaitem-artwork
:size="600"
url=""
:url="image ?? ''"
></mediaitem-artwork>
</div>
<div class="controls-parents">
<template v-if="app.mkReady()">
<div class="app-playback-controls" @mouseover="app.chrome.progresshover = true"
@mouseleave="app.chrome.progresshover = false" @contextmenu="app.nowPlayingContextMenu">
<div class="playback-info">
<div class="song-name">
{{ app.mk.nowPlayingItem["attributes"]["name"] }}
</div>
<div class="col">
<div class="fs-info">
<div>Name</div>
<div>Name</div>
<div>Name</div>
<div
style="display: inline-block; -webkit-box-orient: horizontal; white-space: nowrap;">
<div class="item-navigate song-artist" style="display: inline-block;"
@click="app.getNowPlayingItemDetailed(`artist`)">
{{ app.mk.nowPlayingItem["attributes"]["artistName"] }}
</div>
<div class="song-artist item-navigate" style="display: inline-block;"
@click="app.getNowPlayingItemDetailed('album')">
{{ (app.mk.nowPlayingItem["attributes"]["albumName"]) ? (" - " +
app.mk.nowPlayingItem["attributes"]["albumName"]) : "" }}
</div>
</div>
<div class="song-progress">
<div class="song-duration" style="justify-content: space-between; height: 1px;"
:style="[app.chrome.progresshover ? {'display': 'flex'} : {'display' : 'none'} ]">
<p style="width: auto">{{ app.convertToMins(app.getSongProgress()) }}</p>
<p style="width: auto">{{ app.convertToMins(app.mk.currentPlaybackDuration) }}</p>
</div>
<input type="range" step="0.01" min="0" :style="app.progressBarStyle()"
@input="app.playerLCD.desiredDuration = $event.target.value;app.playerLCD.userInteraction = true"
@mouseup="app.mk.seekToTime($event.target.value);app.playerLCD.desiredDuration = 0;app.playerLCD.userInteraction = false"
:max="app.mk.currentPlaybackDuration" :value="app.getSongProgress()">
</div>
</div>
<div class="control-buttons">
<div class="app-chrome-item">
<button class="playback-button--small shuffle" v-if="app.mk.shuffleMode == 0"
@click="app.mk.shuffleMode = 1"></button>
<button class="playback-button--small shuffle active" v-else
@click="app.mk.shuffleMode = 0"></button>
</div>
<div class="app-chrome-item">
<button class="playback-button previous" @click="app.prevButton()"></button>
</div>
<div class="app-chrome-item">
<button class="playback-button pause" @click="app.mk.pause()" v-if="app.mk.isPlaying"></button>
<button class="playback-button play" @click="app.mk.play()" v-else></button>
</div>
<div class="app-chrome-item">
<button class="playback-button next" @click="app.mk.skipToNextItem()"></button>
</div>
<div class="app-chrome-item">
<button class="playback-button--small repeat" v-if="app.mk.repeatMode == 0"
@click="app.mk.repeatMode = 1"></button>
<button class="playback-button--small repeat active" @click="app.mk.repeatMode = 2"
v-else-if="app.mk.repeatMode == 1"></button>
<button class="playback-button--small repeat repeatOne" @click="app.mk.repeatMode = 0"
v-else-if="app.mk.repeatMode == 2"></button>
</div>
</div>
</div>
</template>
</div>
</div>
<div class="col right-col" v-if="tabMode != ''">
<!-- <div class="fs-info">
<div>Name</div>
<div>Name</div>
<div>Name</div>
</div> -->
<div class="lyrics-col" v-if="tabMode == 'lyrics'">
<lyrics-view :yoffset="120" :time="time" :lyrics="lyrics"
:richlyrics="richlyrics"></lyrics-view>
</div>
<div class="queue-col" v-if="tabMode == 'queue'">
<cider-queue v-if="tabMode == 'queue'" ref="queue" ></cider-queue>
</div>
</div>
</div>
<div class="tab-toggles">
<div class="lyrics" :class="{active: tabMode == 'lyrics'}" @click="tabMode = (tabMode == 'lyrics') ? '' : 'lyrics'"></div>
<div class="queue" :class="{active: tabMode == 'queue'}" @click="tabMode = (tabMode == 'queue') ? '' :'queue'"></div>
</div>
</div>
</div>
</script>
<script>
Vue.component('fullscreen-view', {
template: '#fullscreen-view',
props: {
time: {
type: Number,
required: false
},
lyrics: {
type: Array,
required: false
},
richlyrics: {
type: Array,
required: false
},
image: {
type: String,
required: false
},
},
data: function () {
return {
app: this.$root,
tabMode: "lyrics",
}
},
methods: {
sayHello: function () {
alert('Hello world!');

View file

@ -36,7 +36,7 @@
},
methods: {
visibilityChanged: function (isVisible, entry) {
this.isVisible = isVisible
// this.isVisible = isVisible
},
}
})

View file

@ -42,16 +42,15 @@
<script>
Vue.component('lyrics-view', {
template: '#lyrics-view',
props: ["time", "lyrics", "richlyrics", "translation", "onindex"],
props: ["time", "lyrics", "richlyrics", "translation", "onindex", "yoffset"],
data: function () {
return {
app: this.$root,
}
},
watch: {
time: function () {
if (app.lyricon && app.drawer.open && this.$refs.lyricsview) {
if (((app.lyricon && app.drawer.open) || app.appMode == 'fullscreen') && this.$refs.lyricsview) {
let currentLine = this.$refs.lyricsview.querySelector(`.lyric-line.active`)
if (currentLine && currentLine.getElementsByClassName('lyricWaiting').length > 0) {
let duration = currentLine.getAttribute("end") - currentLine.getAttribute("start");
@ -119,21 +118,40 @@
if(startTime != 9999999) this.app.seekTo(startTime, false);
},
getActiveLyric() {
// console.log(this.time);
const delayfix = 0.1
const prevLine = app.currentLyricsLine;
for (var i = 0; i < this.lyrics.length; i++) {
if (this.time + delayfix >= this.lyrics[i].startTime && this.time + delayfix <= app.lyrics[i].endTime) {
if (app.currentLyricsLine != i) {
app.currentLyricsLine = i;
if (app.lyricon && app.drawer.open && this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${i}"]`)) {
if (((app.lyricon && app.drawer.open) || app.appMode == 'fullscreen') && this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${i}"]`)) {
if(this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${prevLine}"]`)) {this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${prevLine}"]`).classList.remove("active");}
this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${i}"]`).classList.add("active")
if (checkIfScrollIsStatic) {
this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${i}"]`).scrollIntoView({
behavior: "smooth",
block: "center"
if (this.checkIfScrollIsStatic) {
let lyricElement = this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${i}"]`)
// this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${i}"]`).scrollIntoView({
// behavior: "smooth",
// block: "nearest", inline: 'start'
// })
let parent = lyricElement.parentElement
let parentRect = parent.getBoundingClientRect()
let lyricElementRect = lyricElement.getBoundingClientRect()
let parentScrollTop = parent.scrollTop
let parentScrollLeft = parent.scrollLeft
let parentScrollTopDiff = parentScrollTop - parentRect.top
let parentScrollLeftDiff = parentScrollLeft - parentRect.left
let lyricElementScrollTop = lyricElementRect.top + parentScrollTopDiff
let lyricElementScrollLeft = lyricElementRect.left + parentScrollLeftDiff
let scrollTopDiff = lyricElementScrollTop - parentScrollTop
let scrollLeftDiff = lyricElementScrollLeft - parentScrollLeft
let scrollTop = parent.scrollTop + scrollTopDiff
let scrollLeft = parent.scrollLeft + scrollLeftDiff
parent.scrollTo({
top: scrollTop - (this.yoffset ?? 128),
left: scrollLeft,
behavior: 'smooth'
})
}
}
} else if (app.currentLyricsLine == 0 && app.drawer.open) {
@ -144,7 +162,7 @@
}
}
try{
if (app.drawer.open){
if ((app.drawer.open) || app.appMode == 'fullscreen'){
try{this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${prevLine}"]`).childNodes.classList.remove("verse-active");} catch(e){}
for (child of this.$refs.lyricsview.querySelector(`.lyric-line[line-index="${app.currentLyricsLine}"]`).querySelectorAll(".verse")){
if (this.time + 0.1 >= child.getAttribute("lyricstart") * 1 + child.getAttribute("versestart") * 1){
@ -164,7 +182,18 @@
return this.richlyrics[index].l
}
else return []
},
checkIfScrollIsStatic : setInterval(() => {
try {
if (position === this.$refs.lyricsview.scrollTop) {
clearInterval(checkIfScrollIsStatic)
// do something
}
position = this.$refs.lyricsview.scrollTop
} catch (e) {
}
}, 50)
,
}
});
</script>

View file

@ -1,5 +1,5 @@
<script type="text/x-template" id="mediaitem-artwork">
<div class="mediaitem-artwork" :class="[{'rounded': (type == 'artists')}, classes]"
<div class="mediaitem-artwork" :class="[{'rounded': (type == 'artists')}, classes]" :key="url"
v-observe-visibility="{callback: visibilityChanged}">
<img :src="app.getMediaItemArtwork(url, size, width)"
decoding="async" loading="lazy"
@ -15,11 +15,11 @@
template: '#mediaitem-artwork',
props: {
size: {
type: String,
type: [String, Number],
default: '120'
},
width: {
type: Number,
type: [String, Number],
required: false
},
url: {

View file

@ -19,7 +19,7 @@
<div class="artwork" v-if="showArtwork == true">
<mediaitem-artwork
:url="item.attributes.artwork ? item.attributes.artwork.url : ''"
size="50"
:size="48"
:type="item.type"></mediaitem-artwork>
<button class="overlay-play" @click="playTrack()"><%- include("../svg/play.svg") %></button>
</div>
@ -56,7 +56,7 @@
{{ item.attributes.genreNames[0] ?? "" }}
</div>
</template>
<div class="duration" v-if="showDuration" @dblclick="app.routeView(item)">
<div class="duration" v-if="displayDuration" @dblclick="app.routeView(item)">
{{ msToMinSec(item.attributes.durationInMillis ?? 0) }}
</div>
</template>
@ -71,7 +71,8 @@
isVisible: false,
addedToLibrary: false,
guid: this.uuidv4(),
app: this.$root
app: this.$root,
displayDuration: true
}
},
props: {
@ -84,7 +85,19 @@
'show-duration': {type: Boolean, default: true},
'contextExt': {type: Object, required: false},
},
mounted() {
let duration = this.item.attributes.durationInMillis ?? 0
if (duration == 0 || !this.showDuration) {
this.displayDuration = false
}
},
methods: {
dragStart(evt) {
evt.dataTransfer.setData('text/plain', JSON.stringify({
type: this.item.attributes.playParams.kind ?? this.item.type,
id: this.item.id
}))
},
uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
@ -166,7 +179,7 @@
}
}
},
contextMenu(event) {
async contextMenu(event) {
let self = this
let data_type = this.getDataType()
let item_id = this.item.attributes.playParams.id ?? this.item.id
@ -234,20 +247,12 @@
normal: {
items: [
{
"icon": "./assets/feather/list.svg",
"name": "Add to Playlist...",
"action": function () {
app.promptAddToPlaylist()
}
},
{
"name": "Start Radio",
"action": function () {
app.mk.setStationQueue({song: self.item.attributes.playParams.id ?? self.item.id}).then(() => {
app.mk.play()
app.selectedMediaItems = []
})
}
},
{
"name": "Play Next",
"action": function () {
@ -265,17 +270,72 @@
}
},
{
"icon": "./assets/feather/radio.svg",
"name": "Start Radio",
"action": function () {
app.mk.setStationQueue({song: self.item.attributes.playParams.id ?? self.item.id}).then(() => {
app.mk.play()
app.selectedMediaItems = []
})
}
},
{
"icon": "./assets/feather/heart.svg",
"id": "love",
"name": "Love",
"disabled": true,
"action": function () {
app.love(self.item)
}
},
{
"icon": "./assets/feather/x-circle.svg",
"id": "unlove",
"name": "Unlove",
"disabled": true,
"action": function () {
app.unlove(self.item)
}
},
{
"icon": "./assets/feather/thumbs-down.svg",
"id": "dislike",
"name": "Dislike",
"disabled": true,
"action": function () {
app.dislike(self.item)
}
},
{
"icon": "./assets/feather/x-circle.svg",
"id": "undo_dislike",
"name": "Undo dislike",
"disabled": true,
"action": function () {
app.unlove(self.item)
}
},
{
"icon": "./assets/feather/user.svg",
"name": "Go to Artist",
"action": function () {
app.searchAndNavigate(self.item, 'artist')
}
},
{
"icon": "./assets/feather/disc.svg",
"name": "Go to Album",
"action": function () {
app.searchAndNavigate(self.item, 'album')
}
},
{
"icon": "./assets/feather/share.svg",
"name": "Share",
"action": function () {
self.app.copyToClipboard(self.item.attributes.url)
}
}
]
}
}
@ -288,6 +348,19 @@
menus.multiple.items = menus.multiple.items.concat(this.contextExt.multiple)
}
}
try{
let rating = await app.getRating(self.item).catch();
if (rating) {
if(rating == 0) {
menus.normal.items.find(x => x.id == 'love').disabled = false
menus.normal.items.find(x => x.id == 'dislike').disabled = false
}else if(rating == 1) {
menus.normal.items.find(x => x.id == 'unlove').disabled = false
}else if(rating == -1) {
menus.normal.items.find(x => x.id == 'undo_dislike').disabled = false
}
}
} catch(_){}
CiderContextMenu.Create(event, menus[useMenu])
},
visibilityChanged: function (isVisible, entry) {

View file

@ -1,10 +1,10 @@
<script type="text/x-template" id="mediaitem-scroller-horizontal-sp">
<div class="cd-hmedia-scroller hmedia-scroller-card">
<template>
<div class="cd-hmedia-scroller">
<mediaitem-square-sp :item="item"
v-for="item in items"></mediaitem-square-sp>
</div>
<mediaitem-square kind="card" :item="item" size="300"
v-for="item in items"></mediaitem-square>
</template>
</div>
</script>
<script>

View file

@ -1,5 +1,4 @@
<script type="text/x-template" id="mediaitem-square-large">
<template>
<div ref="main" style="position: relative; display: inline-flex;" @contextmenu="contextMenu">
<div @click.self='app.routeView(item)'
class="cd-mediaitem-square-large" ref="main2">
@ -67,7 +66,6 @@
</div>
</div>
</div>
</template>
</script>
<script>
@ -76,6 +74,8 @@
props: ['item'],
data: function () {
return {
isVisible: false,
addedToLibrary: false,
app: this.$root,
}
},
@ -92,14 +92,47 @@
0 /*left*/, null
);
this.$refs.main.dispatchEvent(evt);
},
async isInLibrary() {
if (this.item.type && !this.item.type.includes("library")) {
var params = {
"fields[playlists]": "inLibrary",
"fields[albums]": "inLibrary",
"relate": "library",
"extend": this.revisedRandId()
}
, contextMenu(event) {
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);
this.addedToLibrary = (res && res.attributes && res.attributes.inLibrary) ? res.attributes.inLibrary : false
} else {
this.addedToLibrary = true
}
},
async removeFromLibrary(id) {
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);
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})
this.addedToLibrary = true
},
async contextMenu(event) {
if (!event) { event = this.$refs.main } else { console.log(event) }
let self = this
let useMenu = "normal"
await this.isInLibrary()
if (app.selectedMediaItems.length <= 1) {
app.selectedMediaItems = []
app.select_selectMediaItem(this.item.attributes.playParams.id ?? this.item.id, this.item.attributes.playParams.kind ?? this.item.type, this.index, this.guid)
app.select_selectMediaItem(this.item.attributes.playParams.id ?? this.item.id, this.item.attributes.playParams.kind ?? this.item.type, this.index, this.guid, this.item.attributes.playParams.isLibrary ?? false)
} else {
useMenu = "multiple"
}
@ -154,7 +187,7 @@
{
"name": "Play Next",
"action": function () {
app.mk.playNext({ [self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id })
app.mk.playNext({[self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id})
app.mk.queue._reindex()
app.selectedMediaItems = []
}
@ -162,11 +195,41 @@
{
"name": "Play Later",
"action": function () {
app.mk.playLater({ [self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id })
app.mk.playLater({[self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id})
app.mk.queue._reindex()
app.selectedMediaItems = []
}
},
{
"id": "addToPlaylist",
"name": "Add to Playlist...",
"action": function () {
app.promptAddToPlaylist()
}
},
{
"name": (this.addedToLibrary) ? "Remove from Library..." : "Add to Library...",
"action": async function () {
let item_id = self.item.attributes.playParams.id ?? self.item.id;
let data_type = self.item.attributes.playParams.kind ?? self.item.type;
if (self.addedToLibrary != true) {
console.log("add");
app.addToLibrary(item_id);
self.addedToLibrary = true
} else {
console.log("remove");
await self.removeFromLibrary(item_id);
self.addedToLibrary = false
}
;
}
},
{
"name": "Share",
"action": function () {
self.app.copyToClipboard(self.item.attributes.url)
}
}
]
}
}

View file

@ -144,7 +144,7 @@
await this.isInLibrary()
if (app.selectedMediaItems.length <= 1) {
app.selectedMediaItems = []
app.select_selectMediaItem(this.item.attributes.playParams.id ?? this.item.id, this.item.attributes.playParams.kind ?? this.item.type, this.index, this.guid)
app.select_selectMediaItem(this.item.attributes.playParams.id ?? this.item.id, this.item.attributes.playParams.kind ?? this.item.type, this.index, this.guid, this.item.attributes.playParams.isLibrary ?? false)
} else {
useMenu = "multiple"
}
@ -214,11 +214,44 @@
}
},
{
"id": "addToPlaylist",
"name": "Add to Playlist...",
"action": function () {
app.promptAddToPlaylist()
}
},
{
"id": "love",
"name": "Love",
"disabled": true,
"action": function () {
app.love(self.item)
}
},
{
"id": "unlove",
"name": "Unlove",
"disabled": true,
"action": function () {
app.unlove(self.item)
}
},
{
"id": "dislike",
"name": "Dislike",
"disabled": true,
"action": function () {
app.dislike(self.item)
}
},
{
"id": "undo_dislike",
"name": "Undo dislike",
"disabled": true,
"action": function () {
app.unlove(self.item)
}
},
{
"name": (this.addedToLibrary) ? "Remove from Library..." : "Add to Library...",
"action": async function () {
@ -232,7 +265,21 @@
]
}
}
if ((self.item.attributes.playParams.kind ?? self.item.type).includes("playlist")) { menus.normal.items.splice(2, 1);}
let rating = await app.getRating(self.item)
if(rating == 0) {
menus.normal.items.find(x => x.id == 'love').disabled = false
menus.normal.items.find(x => x.id == 'dislike').disabled = false
}else if(rating == 1) {
menus.normal.items.find(x => x.id == 'unlove').disabled = false
}else if(rating == -1) {
menus.normal.items.find(x => x.id == 'undo_dislike').disabled = false
}
if ((self.item.attributes.playParams.kind ?? self.item.type).includes("playlist")) {
// remove the add to playlist option by id "addToPlaylist" using the .filter() method
menus.normal.items = menus.normal.items.filter(function (item) {
return item.id != "addToPlaylist"
})
}
CiderContextMenu.Create(event, menus[useMenu])
},
}

View file

@ -1,7 +1,9 @@
<script type="text/x-template" id="mediaitem-square">
<div tabindex="0"
class="cd-mediaitem-square" :class="getClasses()" @contextmenu="contextMenu"
v-observe-visibility="{callback: visibilityChanged}">
v-observe-visibility="{callback: visibilityChanged}"
:style="{'--spcolor': getBgColor()}"
@click.self='app.routeView(item)'>
<template v-if="isVisible">
<div class="artwork-container">
<div class="artwork" @click='app.routeView(item)'>
@ -16,15 +18,25 @@
@click="contextMenu"><%- include("../svg/more.svg") %></button>
<button class="play-btn" v-if="!noplay.includes(item.type)"
@click="app.playMediaItem(item)"><%- include("../svg/play.svg") %></button>
<div class="badge-container" v-if="itemBadges.length != 0">
<div class="socialBadge" v-for="badge in itemBadges.limit(1)">
<mediaitem-artwork
:url="(badge.attributes.artwork ? badge.attributes.artwork.url : '')"
:size="32"></mediaitem-artwork>
</div>
<div class="title item-navigate text-overflow-elipsis" @click.self='app.routeView(item)'>
{{ item.attributes.name }}
</div>
<div class="subtitle item-navigate text-overflow-elipsis" @click="app.searchAndNavigate(item,'artist')"
v-if="item.attributes.artistName">
{{ item.attributes.artistName }}
</div>
<div class="subtitle" v-else>&nbsp;</div>
<div class="info-rect" :class="{'info-rect-card': kind == 'card'}" :style="{'--bgartwork': getArtworkUrl(size, true)}">
<div class="title" v-if="item.attributes.artistNames == null || kind!= 'card'" @click='app.routeView(item)'>
<div class="item-navigate text-overflow-elipsis">{{ item.attributes.name }}</div>
<div v-if="item.attributes && item.attributes.contentRating == 'explicit'" style= "margin-top: -2.6px;margin-left: 3px;">🅴</div>
</div>
<div class="subtitle item-navigate text-overflow-elipsis" @click="getSubtitleNavigation()"
v-if="getSubtitle() != ''">
{{ getSubtitle() }}
</div>
<div class="subtitle" v-if="getSubtitle() == '' && kind != 'card'">&nbsp;</div>
</div>
</template>
</div>
</script>
@ -43,8 +55,9 @@
},
size: {
type: String,
default: '300'
}
default: '190'
},
'contextExt': {type: Object, required: false},
},
data: function () {
return {
@ -53,16 +66,105 @@
guid: this.uuidv4(),
noplay: ["apple-curators"],
nomenu: ["artists", "stations", "apple-curators"],
app: this.$root
app: this.$root,
badges: this.$root.socialBadges.badgeMap,
itemBadges: []
}
},
async mounted() {
await this.getBadges()
},
methods: {
getBgColor() {
let color = `#${(this.item.attributes.artwork.bgColor != null) ? (this.item.attributes.artwork.bgColor) : `333333`}`
let c = color.substring(1); // strip #
var rgb = parseInt(c, 16); // convert rrggbb to decimal
var r = (rgb >> 16) & 0xff; // extract red
var g = (rgb >> 8) & 0xff; // extract green
var b = (rgb >> 0) & 0xff; // extract blue
var luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
console.log(color)
console.log(luma)
if (luma > 140) {
return "#aaaaaa"
}else{
return color
}
},
getSubtitle() {
if(this.kind == 'card') {
try {
if (typeof this.item.attributes.artistNames != "undefined") {
return this.item.attributes.artistNames
} else if (typeof this.item.attributes.editorialNotes != "undefined") {
return this.item.attributes.editorialNotes.short
} else if (typeof this.item.attributes.artistName != "undefined") {
return this.item.attributes.artistName
} else {
return ''
}
}catch(e) {
return ''
}
}else {
if (typeof this.item.attributes.artistName != "undefined") {
return this.item.attributes.artistName
} else {
return ''
}
}
},
getSubtitleNavigation() {
if(this.kind == 'card') {
try {
if (typeof this.item.attributes.artistNames != "undefined") {
return app.routeView(this.item)
} else if (typeof this.item.attributes.editorialNotes != "undefined") {
return app.routeView(this.item)
} else if (typeof this.item.attributes.artistName != "undefined") {
return app.searchAndNavigate(this.item,'artist')
} else {
return app.routeView(this.item)
}
}catch(e) {
return app.routeView(this.item)
}
}else {
if (typeof this.item.attributes.artistName != "undefined") {
return app.searchAndNavigate(this.item,'artist')
} else {
return app.routeView(this.item)
}
}
},
async getBadges() {
let self = this
if (this.badges[this.item.attributes.playParams.id ?? this.item.id]) {
let friends = this.badges[this.item.attributes.playParams.id ?? this.item.id]
if (friends) {
friends.forEach(function (friend) {
self.app.mk.api.socialProfile(friend).then(data => {
self.itemBadges.push(data)
})
})
}
}
},
revisedRandId() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10);
},
async isInLibrary() {
if (this.item.type && !this.item.type.includes("library")) {
var params = {"fields[playlists]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library", "extend": this.revisedRandId()}
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);
this.addedToLibrary = (res && res.attributes && res.attributes.inLibrary) ? res.attributes.inLibrary : false
} else {
@ -70,7 +172,13 @@
}
},
async removeFromLibrary(id) {
var params = {"fields[playlists]": "inLibrary","fields[songs]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library", "extend": this.revisedRandId()}
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);
if (res && res.relationships && res.relationships.library && res.relationships.library.data && res.relationships.library.data.length > 0) {
@ -86,14 +194,21 @@
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
},
getArtworkUrl() {
getArtworkUrl(size = -1, includeUrl = false) {
let artwork = this.item.attributes.artwork ? this.item.attributes.artwork.url : ''
if(size != -1) {
artwork = artwork.replace('{w}', size).replace('{h}', size).replace('{f}', "webp").replace('{c}', ((size === 900) ? "sr" : "cc"))
}
switch (this.kind) {
case "385":
artwork = this.item.attributes.editorialArtwork.subscriptionHero.url
break;
}
if(!includeUrl) {
return artwork
}else{
return `url("${artwork}")`
}
},
getClasses() {
let type = this.item.type
@ -104,6 +219,9 @@
default:
return []
break;
case "card":
return ["mediaitem-card"]
break;
case "385": // editorial
return ["mediaitem-brick"]
break;
@ -134,7 +252,7 @@
let useMenu = "normal"
if (app.selectedMediaItems.length <= 1) {
app.selectedMediaItems = []
app.select_selectMediaItem(this.item.attributes.playParams.id ?? this.item.id, this.item.attributes.playParams.kind ?? this.item.type, this.index, this.guid)
app.select_selectMediaItem(this.item.attributes.playParams.id ?? this.item.id, this.item.attributes.playParams.kind ?? this.item.type, this.index, this.guid, this.item.attributes.playParams.isLibrary ?? false)
} else {
useMenu = "multiple"
}
@ -186,6 +304,14 @@
},
normal: {
items: [
{
"icon": "./assets/feather/list.svg",
"id": "addToPlaylist",
"name": "Add to Playlist...",
"action": function () {
app.promptAddToPlaylist()
}
},
{
"name": "Play Next",
"action": function () {
@ -203,31 +329,100 @@
}
},
{
"name": "Add to Playlist...",
"action": function () {
app.promptAddToPlaylist()
}
},
{
"icon": "./assets/feather/plus.svg",
"name": (this.addedToLibrary) ? "Remove from Library..." : "Add to Library...",
"action": async function () {
let item_id = self.item.attributes.playParams.id ?? self.item.id;
let data_type = self.item.attributes.playParams.kind ?? self.item.type;
if (self.addedToLibrary != true) { console.log("add"); app.addToLibrary(item_id); self.addedToLibrary = true}
else { console.log("remove"); await self.removeFromLibrary(item_id); self.addedToLibrary = false};
if (self.addedToLibrary != true) {
console.log("add");
app.addToLibrary(item_id);
self.addedToLibrary = true
} else {
console.log("remove");
await self.removeFromLibrary(item_id);
self.addedToLibrary = false
}
;
}
},
{
"icon": "./assets/feather/heart.svg",
"id": "love",
"name": "Love",
"disabled": true,
"action": function () {
app.love(self.item)
}
},
{
"icon": "./assets/feather/x-circle.svg",
"id": "unlove",
"name": "Unlove",
"disabled": true,
"action": function () {
app.unlove(self.item)
}
},
{
"icon": "./assets/feather/thumbs-down.svg",
"id": "dislike",
"name": "Dislike",
"disabled": true,
"action": function () {
app.dislike(self.item)
}
},
{
"icon": "./assets/feather/x-circle.svg",
"id": "undo_dislike",
"name": "Undo dislike",
"disabled": true,
"action": function () {
app.unlove(self.item)
}
},
{
"icon": "./assets/feather/share.svg",
"name": "Share",
"action": function () {
self.app.copyToClipboard(self.item.attributes.url)
}
}
]
}
}
if ((self.item.attributes.playParams.kind ?? self.item.type).includes("playlist")) { menus.normal.items.splice(2, 1);}
if ((self.item.attributes.playParams.kind ?? self.item.type).includes("playlist")) {
// remove the add to playlist option by id "addToPlaylist" using the .filter() method
menus.normal.items = menus.normal.items.filter(function (item) {
return item.id != "addToPlaylist"
})
}
let rating = await app.getRating(self.item)
if (rating == 0) {
menus.normal.items.find(x => x.id == 'love').disabled = false
menus.normal.items.find(x => x.id == 'dislike').disabled = false
} else if (rating == 1) {
menus.normal.items.find(x => x.id == 'unlove').disabled = false
} else if (rating == -1) {
menus.normal.items.find(x => x.id == 'undo_dislike').disabled = false
}
if (this.contextExt) {
if (this.contextExt.normal) {
menus.normal.items = menus.normal.items.concat(this.contextExt.normal)
}
if (this.contextExt.multiple) {
menus.multiple.items = menus.multiple.items.concat(this.contextExt.multiple)
}
}
CiderContextMenu.Create(event, menus[useMenu])
},
},
beforeDestroy: function () {
this.item = null;
this.kind = null;
this.size = null;
// this.item = null;
// this.kind = null;
// this.size = null;
}
});
</script>

View file

@ -0,0 +1,221 @@
<script type="text/x-template" id="sidebar-playlist">
<div class="sidebar-playlist" :key="item.id">
<button class="app-sidebar-item app-sidebar-item-playlist" :key="item.id" v-if="item.type != 'library-playlist-folders'"
:class="{'active': $root.page.includes(item.id)}"
@contextmenu="playlistContextMenu($event, item.id)"
@dragstart="startDrag($event, item)"
@dragover="dragOver"
@drop="onDrop"
:href="item.href"
@click='$root.appRoute(`playlist_` + item.id); $root.showingPlaylist = [];$root.getPlaylistFromID($root.page.substring(9))'>
<template v-if="!renaming">
<div class="sidebar-icon" v-html="icon"></div> {{ item.attributes.name }}
</template>
<input type="text" v-model="item.attributes.name" class="pl-rename-field" @blur="rename()" @keydown.enter="rename()" v-else>
</button>
<button class="app-sidebar-item app-sidebar-item-playlist" :key="item.id" v-else
:class="[{'folder-button-active': folderOpened}, isPlaylistSelected]"
@contextmenu="playlistContextMenu($event, item.id)"
@dragstart="startDrag($event, item)"
@dragover="dragOver"
@drop="onDrop"
:href="item.href"
@click='getPlaylistChildren(item)'>
<span v-if="!folderOpened">📁</span>
<span v-else>📂</span>
<template v-if="!renaming">
{{ item.attributes.name }}
</template>
<input type="text" v-model="item.attributes.name" class="pl-rename-field" @blur="rename()" @keydown.enter="rename()" v-else>
</button>
<div class="folder-body" v-if="item.type === 'library-playlist-folders' && folderOpened">
<template v-if="children.length != 0">
<sidebar-playlist v-for="item in children" :item="item" :key="item.id"></sidebar-playlist>
</template>
<template v-else>
<div class="spinner"></div>
</template>
</div>
</div>
</script>
<script>
Vue.component('sidebar-playlist', {
template: '#sidebar-playlist',
props: {
item: {
type: Object,
required: true
}
},
data: function () {
return {
folderOpened: false,
children: [],
playlistRoot: "p.playlistsroot",
renaming: false,
icon: ""
}
},
async mounted() {
this.icon = await this.$root.getSvgIcon("./assets/feather/list.svg")
},
methods: {
rename() {
this.renaming = false
if(this.item.type === "library-playlist-folders") {
this.$root.editPlaylistFolder(this.item.id, this.item.attributes.name)
}else{
this.$root.editPlaylist(this.item.id, this.item.attributes.name)
}
},
async getChildren() {
let self = this
this.children = []
this.children = this.$root.playlists.listing.filter(child => {
if(child.parent == self.item.id) {
return child
}
})
},
async move(item, sendTo) {
let self = this
console.log(sendTo)
let type = item.type.replace("library-", "")
let typeTo = sendTo.type
this.$root.mk.api.v3.music(`/v1/me/library/${type}/${item.id}/parent`, {}, {
fetchOptions: {
method: "PUT",
body: JSON.stringify({
data: [{
id: sendTo.id,
type: typeTo
}]
})
}
})
// find the item in this.$root.playlists.listing and store it in a variable
this.$root.playlists.listing.filter(playlist => {
if(playlist.id == item.id) {
console.log(playlist)
playlist.parent = sendTo.id
}
})
if(typeof this.$parent.getChildren == "function") {
this.$parent.getChildren()
console.log(this.$parent.children)
}
await this.getChildren()
this.$root.sortPlaylists()
// await this.$root.refreshPlaylists()
},
playlistContextMenu(event, playlist_id) {
let menu = {
items: {
"moveToParent": {
name: "Move to top",
action: () => {
let self = this
this.move(this.item, {
id: this.playlistRoot,
type: "library-playlist-folders"
})
setTimeout(()=>{self.getChildren()}, 2000)
}
},
"rename": {
name: "Rename",
action: () => {
this.renaming = true
setTimeout(()=>{
document.querySelector(".pl-rename-field").focus()
document.querySelector(".pl-rename-field").select()
}, 100)
}
},
"deleteFromPlaylist": {
name: "Delete from library",
action: () => {
this.$root.deletePlaylist(playlist_id)
}
},
"addToFavorites": {
name: "Add to favorites",
disabled: true,
action: () => {
this.addFavorite(playlist_id, "library-playlists")
}
}
}
}
if(this.item.type === "library-playlist-folders") {
menu.items.addToFavorites.disabled = true
}
CiderContextMenu.Create(event, menu)
},
dragOver(evt) {
evt.preventDefault();
evt.dataTransfer.dropEffect = "move";
},
onDrop (evt) {
let data = JSON.parse(evt.dataTransfer.getData("text/plain"))
evt.preventDefault();
if(data.id == this.item.id) {
return;
}
console.log(data)
if(data) {
if(this.item.type == "library-playlists" || this.item.type == "library-playlist-folders") {
if(data.type == "library-playlists" && this.item.type == "library-playlists") {
return
}
this.move(data, this.item)
}
}
},
startDrag (evt) {
evt.dataTransfer.dropEffect = 'move'
evt.dataTransfer.effectAllowed = 'move'
evt.dataTransfer.setData('text/plain', JSON.stringify(this.item))
},
getPlaylistChildren(item) {
let self = this
this.children = []
this.getChildren()
this.toggleFolder()
this.$root.mk.api.library.playlistFolderChildren(item.id).then(children => {
children.forEach(child => {
if(!self.$root.playlists.listing.find(listing => listing.id == child.id)) {
child.parent = self.item.id
self.$root.playlists.listing.push(child)
}
})
self.$root.playlists.listing.sort((a, b) => {
if (a.type === 'library-playlist-folders' && b.type !== 'library-playlist-folders') {
return -1
} else if (a.type !== 'library-playlist-folders' && b.type === 'library-playlist-folders') {
return 1
} else {
return 0
}
})
self.getChildren()
})
},
isPlaylistSelected(item) {
if(this.$root.showingPlaylist.id == item.id) {
return ["active"]
} else {
return []
}
},
toggleFolder() {
this.folderOpened = !this.folderOpened;
}
}
});
</script>

View file

@ -0,0 +1,355 @@
<script type="text/x-template" id="spatial-properties">
<div class="modal-fullscreen spatialproperties-panel">
<div class="modal-window" v-if="ready">
<div class="modal-header">
<div class="modal-title">Spatial Properties</div>
<button class="close-btn" @click="close()"></button>
</div>
<div class="modal-content">
<template v-if="roomEditType == 'dimensions'">
<div class="row">
<div class="col"><h3>Room Dimensions</h3></div>
<div class="col-auto flex-center">
<button class="md-btn" @click="roomEditType = 'positions'">Set Positions</button>
</div>
</div>
<div class="row">
<div class="col">
<div class="row">
<div class="col-3 flex-center">
Width
</div>
<div class="col flex-center">
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
v-model="room_dimensions.width" step="1"/>
</div>
<div class="col-3 flex-center">
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
v-model="room_dimensions.width" step="1"/>
</div>
</div>
<div class="row">
<div class="col-3 flex-center">
Height
</div>
<div class="col flex-center">
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
v-model="room_dimensions.height" step="1"/>
</div>
<div class="col-3 flex-center">
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
v-model="room_dimensions.height" step="1"/>
</div>
</div>
<div class="row">
<div class="col-3 flex-center">
Depth
</div>
<div class="col flex-center">
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
v-model="room_dimensions.depth" step="1"/>
</div>
<div class="col-3 flex-center">
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
v-model="room_dimensions.depth" step="1"/>
</div>
</div>
<label v-if="!app.cfg.audio.normalization">
Gain
<input type="number" min="0" @change="setRoom()" style="width: 100%;"
v-model="app.cfg.audio.spatial_properties.gain" step="0.1"/>
</label>
</div>
<div class="col visual-container">
<div class="visual" :style="objectContainerStyle()">
<div class="face" :style="[faceStyle()]"></div>
<div class="face" :style="[faceStyle(), topFaceStyle()]"></div>
</div>
</div>
</div>
</template>
<template v-if="roomEditType == 'positions'">
<div class="row">
<div class="col"><h3>Room Positions</h3></div>
<div class="col-auto flex-center">
<button class="md-btn" @click="roomEditType = 'dimensions'">Set Dimensions</button>
</div>
</div>
<div class="row">
<div class="col">
<div class="row">
<div class="col-3 flex-center">
X (Listener)
</div>
<div class="col flex-center">
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
v-model="listener_position[0]" step="1"/>
</div>
<div class="col-3 flex-center">
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
v-model="listener_position[0]" step="1"/>
</div>
</div>
<div class="row">
<div class="col-3 flex-center">
Y (Listener)
</div>
<div class="col flex-center">
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
v-model="listener_position[1]" step="1"/>
</div>
<div class="col-3 flex-center">
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
v-model="listener_position[1]" step="1"/>
</div>
</div>
<div class="row">
<div class="col-3 flex-center">
Z (Listener)
</div>
<div class="col flex-center">
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
v-model="listener_position[2]" step="1"/>
</div>
<div class="col-3 flex-center">
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
v-model="listener_position[2]" step="1"/>
</div>
</div>
<div class="row">
<div class="col-3 flex-center">
X (Audio Source)
</div>
<div class="col flex-center">
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
v-model="audio_position[0]" step="1"/>
</div>
<div class="col-3 flex-center">
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
v-model="audio_position[0]" step="1"/>
</div>
</div>
<div class="row">
<div class="col-3 flex-center">
Y (Audio Source)
</div>
<div class="col flex-center">
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
v-model="audio_position[1]" step="1"/>
</div>
<div class="col-3 flex-center">
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
v-model="audio_position[1]" step="1"/>
</div>
</div>
<div class="row">
<div class="col-3 flex-center">
Z (Audio Source)
</div>
<div class="col flex-center">
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
v-model="audio_position[2]" step="1"/>
</div>
<div class="col-3 flex-center">
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
v-model="audio_position[2]" step="1"/>
</div>
</div>
</div>
<div class="col visual-container">
<div class="visual">
<div class="face" :style="[faceStyle()]"></div>
<div class="face" :style="[faceStyle(), topFaceStyle()]"></div>
<!-- <div class="listener" :style="[listenerStyle()]">L</div> -->
<!-- <div class="audiosource" :style="[audioSourceStyle()]">A</div> -->
</div>
</div>
</div>
</template>
<div class="row">
<div class="col"><h3>Room Materials</h3></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col flex-center">
<label>
Up
<select class="md-select" @change="setRoom()"
v-model="room_materials.up">
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
</select>
</label>
</div>
<div class="col"></div>
</div>
<div class="row">
<div class="col flex-center">
<label>
Left
<select class="md-select" @change="setRoom()"
v-model="room_materials.left">
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
</select>
</label>
</div>
<div class="col flex-center">
<label>
Front
<select class="md-select" @change="setRoom()"
v-model="room_materials.front">
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
</select>
</label>
<label>
Back
<select class="md-select" @change="setRoom()"
v-model="room_materials.back">
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
</select>
</label>
</div>
<div class="col flex-center">
<label>
Right
<select class="md-select" @change="setRoom()"
v-model="room_materials.right">
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
</select>
</label>
</div>
</div>
<div class="row">
<div class="col"></div>
<div class="col flex-center">
<label>
Down
<select class="md-select" @change="setRoom()"
v-model="room_materials.down">
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
</select>
</label>
</div>
<div class="col"></div>
</div>
</div>
</div>
</div>
</script>
<script>
Vue.component('spatial-properties', {
template: '#spatial-properties',
data: function () {
return {
app: this.$root,
room_dimensions: null,
room_materials: null,
listener_position: null,
audio_position: null,
roomEditType: "dimensions",
roomProps: [
'transparent',
'acoustic-ceiling-tiles',
'brick-bare',
'brick-painted',
'concrete-block-coarse',
'concrete-block-painted',
'curtain-heavy',
'fiber-glass-insulation',
'glass-thin',
'glass-thick',
'grass',
'linoleum-on-concrete',
'marble',
'metal',
'parquet-on-concrete',
'plaster-smooth',
'plywood-panel',
'polished-concrete-or-tile',
'sheetrock',
'water-or-ice-surface',
'wood-ceiling',
'wood-panel',
'uniform'
],
visualMultiplier: 4,
ready: false
}
},
props: {},
mounted() {
this.room_dimensions = JSON.parse(JSON.stringify(this.$root.cfg.audio.spatial_properties.room_dimensions))
this.room_materials = JSON.parse(JSON.stringify(this.$root.cfg.audio.spatial_properties.room_materials))
this.audio_position = JSON.parse(JSON.stringify(this.$root.cfg.audio.spatial_properties.audio_position))
this.listener_position = JSON.parse(JSON.stringify(this.$root.cfg.audio.spatial_properties.listener_position))
if(typeof this.app.mk.nowPlayingItem != "undefined") {
this.setRoom()
}
this.ready = true
},
methods: {
listenerStyle() {
let style = {
transform: `rotateX(60deg) rotateZ(-45deg) translateX(${this.listener_position[0]}px) translateY(${this.listener_position[2]}px) translateZ(${100 + +this.listener_position[1]}px)`
}
return style
},
audioSourceStyle() {
let style = {
transform: `rotateX(60deg) rotateZ(-45deg) translateX(${this.audio_position[0]}px) translateY(${this.audio_position[2]}px) translateZ(${100 + +this.audio_position[1]}px)`
}
return style
},
topFaceStyle() {
let style = {
transform: `rotateX(60deg) rotateZ(-45deg) translateZ(${this.room_dimensions.height * this.visualMultiplier}px)`
}
return style
},
objectContainerStyle() {
let scale = 1
if(this.room_dimensions.width * this.visualMultiplier > 300) {
scale = 300 / (this.room_dimensions.width * this.visualMultiplier)
}
let style = {
transform: `scale(${scale})`
}
return style
},
faceStyle() {
let style = {
width: `${this.room_dimensions.width * this.visualMultiplier}px`,
height: `${this.room_dimensions.depth * this.visualMultiplier}px`,
}
return style
},
close() {
this.$root.cfg.audio.spatial_properties.room_dimensions = this.room_dimensions
this.$root.cfg.audio.spatial_properties.room_materials = this.room_materials
this.$root.cfg.audio.spatial_properties.audio_position = this.audio_position
this.$root.cfg.audio.spatial_properties.listener_position = this.listener_position
app.resetState()
},
setRoom() {
window.CiderAudio.audioNodes.spatialNode.setRoomProperties(this.room_dimensions, this.room_materials);
CiderAudio.audioNodes.spatialInput.setPosition(...this.audio_position)
CiderAudio.audioNodes.spatialNode.setListenerPosition(...this.listener_position)
if(!this.app.cfg.audio.normalization) {
window.CiderAudio.audioNodes.gainNode.gain.value = app.cfg.audio.spatial_properties.gain
}
}
}
});
</script>

View file

@ -18,9 +18,11 @@
<link rel="stylesheet/less" type="text/css" href="style.less"/>
<script src="less.js"></script>
<script src="<%- (env.dev ? "vue.js" : "vue.dev.js") %>"></script>
<script src="vuex.min.js"></script>
<script src="sortable.min.js"></script>
<script src="vue-observe-visibility.min.js"></script>
<script src="sortable.min.js"></script>
<script src="vibrant.min.js"></script>
<script src="vuedraggable.umd.min.js"></script>
<link rel="manifest" href="./manifest.json?v=2">
<script src="https://js-cdn.music.apple.com/hls.js/2.141.0/hls.js/hls.js"></script>
@ -67,7 +69,9 @@
<template v-if="mkReady()">
<div class="app-playback-controls" @mouseover="chrome.progresshover = true"
@mouseleave="chrome.progresshover = false" @contextmenu="nowPlayingContextMenu">
<div class="artwork"></div>
<div class="artwork" @click="drawer.open = false; appMode = 'fullscreen'">
<mediaitem-artwork :url="currentArtUrl"></mediaitem-artwork>
</div>
<div class="playback-info">
<div class="song-name">
{{ mk.nowPlayingItem["attributes"]["name"] }}
@ -114,7 +118,7 @@
<div class="app-chrome--right">
<div class="app-chrome-item volume display--large">
<div class="app-chrome-item volume-icon"></div>
<input type="range" class="" step="0.01" min="0" max="1" v-model="mk.volume"
<input type="range" class="" @wheel="volumeWheel" step="0.01" min="0" max="1" v-model="mk.volume"
v-if="typeof mk.volume != 'undefined'">
</div>
<div class="app-chrome-item generic" v-if="false">
@ -168,29 +172,21 @@
<div class="app-sidebar-header-text">
Apple Music
</div>
<sidebar-library-item v-if="isDev" name="Home" page="home"></sidebar-library-item>
<sidebar-library-item name="Listen Now" page="listen_now"></sidebar-library-item>
<sidebar-library-item name="Browse" page="browse"></sidebar-library-item>
<sidebar-library-item name="Radio" page="radio"></sidebar-library-item>
<sidebar-library-item name="Home" svg-icon="./assets/feather/home.svg" page="home"></sidebar-library-item>
<sidebar-library-item name="Listen Now" svg-icon="./assets/feather/play-circle.svg" page="listen_now"></sidebar-library-item>
<sidebar-library-item name="Browse" svg-icon="./assets/feather/globe.svg" page="browse"></sidebar-library-item>
<sidebar-library-item name="Radio" svg-icon="./assets/feather/radio.svg" page="radio"></sidebar-library-item>
<div class="app-sidebar-header-text">
Library
</div>
<sidebar-library-item name="Recently Added" page="library-recentlyadded"></sidebar-library-item>
<sidebar-library-item name="Songs" page="library-songs"></sidebar-library-item>
<sidebar-library-item name="Albums" page="library-albums"></sidebar-library-item>
<sidebar-library-item name="Artists" page="library-artists"></sidebar-library-item>
<sidebar-library-item name="Made For You" page="library-madeforyou"></sidebar-library-item>
<sidebar-library-item name="Recently Added" svg-icon="./assets/feather/plus-circle.svg" page="library-recentlyadded"></sidebar-library-item>
<sidebar-library-item name="Songs" svg-icon="./assets/feather/music.svg" page="library-songs"></sidebar-library-item>
<sidebar-library-item name="Albums" svg-icon="./assets/feather/disc.svg" page="library-albums"></sidebar-library-item>
<sidebar-library-item name="Artists" svg-icon="./assets/feather/user.svg" page="library-artists"></sidebar-library-item>
<div class="app-sidebar-header-text" @contextmenu="playlistHeaderContextMenu">
Playlists
</div>
<button class="app-sidebar-item" v-for="item in playlists.listing" :key="item.id"
v-if="item.attributes.name"
@contextmenu="playlistContextMenu($event, item.id)"
@dragover="()=>{}"
:href="item.href"
@click='appRoute(`playlist_` + item.id); showingPlaylist = [];getPlaylistFromID(page.substring(9))'>
{{ item.attributes.name }}
</button>
<sidebar-playlist v-for="item in getPlaylistFolderChildren('p.playlistsroot')" :item="item"></sidebar-playlist>
</div>
<transition name="wpfade">
<div class="usermenu-container" v-if="chrome.menuOpened">
@ -215,6 +211,9 @@
</div>
</div>
</button>
<button class="usermenu-item" v-if="cfg.advanced.AudioContext && cfg.audio.spatial" @click="modals.spatialProperties = true">
Spatialized Audio Settings
</button>
<button class="usermenu-item">
About
</button>
@ -232,7 +231,7 @@
</transition>
<div class="app-sidebar-footer">
<input type="range" class="web-slider display--small" step="0.01" min="0" max="1"
v-model="mk.volume"
v-model="mk.volume" @wheel="volumeWheel"
v-if="typeof mk.volume != 'undefined'">
<button class="app-sidebar-button" style="width:100%"
:class="{active: chrome.menuOpened}"
@ -302,6 +301,12 @@
<cider-home></cider-home>
</template>
</transition>
<!-- Home -->
<transition name="wpfade">
<template v-if="page == 'artist-feed'">
<cider-artist-feed></cider-artist-feed>
</template>
</transition>
<!-- Playlist / Album page-->
<transition name="wpfade">
<template v-if="page.includes('playlist_')">
@ -443,8 +448,17 @@
</div>
<transition name="drawertransition">
<div class="app-drawer" v-if="drawer.open">
<div class="bgArtworkMaterial">
<div class="bg-artwork-container">
<img class="bg-artwork a" :src="$store.state.artwork.playerLCD">
<img class="bg-artwork b" :src="$store.state.artwork.playerLCD">
</div>
</div>
<lyrics-view v-if="drawer.panel == 'lyrics'" :time="lyriccurrenttime" :lyrics="lyrics"
:richlyrics="richlyrics"></lyrics-view>
<div v-if="drawer.panel == 'lyrics'" class="lyric-footer">
<button class="md-btn" @click="modularUITest(!fullscreenLyrics)">{{fullscreenLyrics ? "Default View":'Fullscreen View'}}</button>
</div>
<cider-queue ref="queue" v-if="drawer.panel == 'queue'"></cider-queue>
</div>
</transition>
@ -453,12 +467,16 @@
</transition>
<transition name="fsModeSwitch">
<div class="fullscreen-view-container" v-if="appMode == 'fullscreen'">
<fullscreen-view></fullscreen-view>
<fullscreen-view :image="currentArtUrl.replace('50x50', '600x600')" :time="lyriccurrenttime" :lyrics="lyrics" :richlyrics="richlyrics"></fullscreen-view>
</div>
</transition>
<transition name="wpfade">
<img v-show="chrome.artworkReady" @load="chrome.artworkReady = true" class="bg-artwork"
<div class="bg-artwork-container" :class="{noanimation: (!cfg.visual.bg_artwork_rotation || !animateBackground)}">
<img @load="chrome.artworkReady = true" class="bg-artwork a "
>
<img class="bg-artwork b"
>
</div>
</transition>
<transition name="wpfade">
<div class="bg-artwork--placeholder"></div>
@ -466,6 +484,9 @@
<transition name="modal">
<add-to-playlist :playlists="playlists.listing" v-if="modals.addToPlaylist"></add-to-playlist>
</transition>
<transition name="modal">
<spatial-properties v-if="modals.spatialProperties"></spatial-properties>
</transition>
<div id="apple-music-video-container">
<div id="apple-music-video-player-controls">
<div id="player-exit" title="Close" @click="app.exitMV()">
@ -512,6 +533,9 @@
<!-- Home -->
<%- include('pages/home') %>
<!-- Artist Feed -->
<%- include('pages/artist-feed') %>
<!-- Playlists / Albums -->
<%- include('pages/cider-playlist') %>
@ -538,10 +562,15 @@
<script type="text/x-template" id="sidebar-library-item">
<button class="app-sidebar-item"
:class="$parent.getSidebarItemClass(page)" @click="$root.appRoute(page)">
<div class="sidebar-icon" v-html="svgIconData" v-if="svgIconData != ''"></div>
{{ name }}
</button>
</script>
<!-- Playlist Listing -->
<%- include('components/sidebar-playlist') %>
<!-- Spatial Properties -->
<%- include('components/spatial-properties') %>
<!-- Add to playlist -->
<%- include('components/add-to-playlist') %>
<!-- Queue -->
@ -585,5 +614,7 @@
}
</script>
<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>
</body>
</html>

View file

@ -0,0 +1,68 @@
<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>Your Artists Feed</h3>
</div>
</div>
<div class="well" style="margin-top:0px;">
<template v-if="artistFeed.length > 0">
<mediaitem-list-item v-for="item in artistFeed" :item="item"></mediaitem-list-item>
</template>
<template v-else>
<div class="spinner"></div>
</template>
</div>
</div>
</div>
</div>
</div>
</script>
<script>
Vue.component('cider-artist-feed', {
template: '#cider-artist-feed',
data: function () {
return {
app: this.$root,
followedArtists: this.$root.cfg.home.followedArtists,
artistFeed: [],
}
},
async mounted() {
let self = this
await this.getArtistFeed()
},
methods: {
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 => {
if (item.views["latest-release"].data.length != 0) {
self.artistFeed.push(item.views["latest-release"].data[0])
}
})
// sort artistFeed by attributes.releaseDate descending, date is formatted as "YYYY-MM-DD"
this.artistFeed.sort((a, b) => {
let dateA = new Date(a.attributes.releaseDate)
let dateB = new Date(b.attributes.releaseDate)
return dateB - dateA
})
})
}
}
});
</script>

View file

@ -12,7 +12,7 @@
<mediaitem-artwork
shadow="large"
:url="data.attributes.artwork ? data.attributes.artwork.url : ''"
size="220" type="artists"></mediaitem-artwork>
size="190" type="artists"></mediaitem-artwork>
<button class="overlay-play" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
app.mk.play()
})">
@ -45,9 +45,9 @@
<div class="col-auto" v-if="data.views['latest-release'].data.length != 0">
<h3>Latest Release</h3>
<div style="width: auto;margin: 0 auto;">
<mediaitem-square-sp v-for="song in data.views['latest-release'].data"
<mediaitem-square kind="card" v-for="song in data.views['latest-release'].data"
:item="song">
</mediaitem-square-sp>
</mediaitem-square>
</div>
</div>
<div class="col" v-if="data.views['top-songs']">
@ -135,6 +135,27 @@
methods: {
artistMenu (event) {
let self = this
let followAction = "follow"
let followActions = {
follow: {
name: "Follow Artist",
action: ()=>{
self.app.cfg.home.followedArtists.push(self.data.id)
}
},
unfollow: {
name: "Unfollow Artist",
action: ()=>{
let index = self.app.cfg.home.followedArtists.indexOf(self.data.id)
if (index > -1) {
self.app.cfg.home.followedArtists.splice(index, 1)
}
}
}
}
if(this.app.cfg.home.followedArtists.includes(self.data.id)) {
followAction = "unfollow"
}
CiderContextMenu.Create(event, {
items: [
{
@ -145,13 +166,12 @@
})
}
},
{
name: "Follow Artist",
action: ()=>{}
},
followActions[followAction],
{
name: "Share",
action: ()=>{}
action: ()=>{
self.app.copyToClipboard(self.data.attributes.url)
}
}
]
})

View file

@ -2,7 +2,9 @@
<div class="content-inner playlist-page" v-if="data != [] && data.attributes != null"
:style="{'--bgColor': (data.attributes.artwork != null && data.attributes.artwork['bgColor'] != null) ? ('#' + data.attributes.artwork.bgColor) : ''}">
<template v-if="app.playlists.loadingState == 0">
<div class="content-inner centered">Loading...</div>
<div class="content-inner centered">
<div class="spinner"></div>
</div>
</template>
<template v-if="app.playlists.loadingState == 1">
<div class="playlist-display row"
@ -83,14 +85,25 @@
</div>
</div>
<div class="playlist-time">
{{data.attributes.releaseDate}}
<div class="friends-info" v-if="itemBadges.length != 0">
<div class="well">
<div class="badge-container">
<div class="socialBadge" :title="`${badge.attributes.name} - @${badge.attributes.handle}`" v-for="badge in itemBadges">
<mediaitem-artwork
:url="badge.attributes.artwork.url"
:size="60"></mediaitem-artwork>
</div>
</div>
</div>
</div>
<div class="playlist-time">
{{getFormattedDate(data.attributes.releaseDate)}}
</div>
<div class="playlist-time total">{{app.getTotalTime()}}</div>
<div class="playlist-time item-navigate" @click="app.searchAndNavigate(data,'recordLabel') "
style="width: 50%;">
{{data.attributes.copyright}}
</div>
<div class="playlist-time">{{app.getTotalTime()}}</div>
</div>
</template>
</div>
@ -106,7 +119,9 @@
drag: false,
nameEditing: false,
inLibrary: null,
app: this.$root
app: this.$root,
itemBadges: [],
badgesRequested: false
}
},
mounted: function () {
@ -117,9 +132,35 @@
watch: {
data: function () {
this.isInLibrary()
this.getBadges()
}
},
methods: {
getBadges() {
return
if(this.badgesRequested) {
return
}
this.badgesRequested = true
this.itemBadges = []
let self = this
var id = 0
try {
id = this.data.attributes.playParams.id
} catch (e) {
id = this.data.id
}
this.$root.getSocialBadges((badges) => {
let friends = badges[id]
if(friends) {
friends.forEach(function (friend) {
self.app.mk.api.socialProfile(friend).then(data => {
self.itemBadges.push(data)
})
})
}
})
},
async isInLibrary() {
if (this.data.type && !this.data.type.includes("library")) {
// please keep using vars here
@ -132,7 +173,12 @@
}
},
editPlaylist() {
app.editPlaylist(this.data.id, this.data.attributes.name);
this.app.editPlaylist(this.data.id, this.data.attributes.name);
this.app.playlists.listing.forEach(playlist => {
if (playlist.id == this.data.id) {
playlist.attributes.name = this.data.attributes.name
}
})
this.nameEditing = false
},
addToLibrary(id) {
@ -225,6 +271,19 @@
kind = data.attributes.playParams.kind;
id = data.attributes.playParams.id;
return `${kind}:${id}`
},
getFormattedDate: function (date) {
if (date == null || date === "") return "";
try {
var releaseDate = new Date(date);
month = new Intl.DateTimeFormat('en-US', {month: 'long'}).format(releaseDate);
date = releaseDate.getDate();
year = releaseDate.getFullYear();
return date + " " + month + " " + year;
} catch (e) {
return ""
}
}
}
})

View file

@ -64,7 +64,11 @@
},
scrollToTop() {
let target = document.querySelector(".header-text")
target.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"})
document.querySelector("#app-content").scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
})
},
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
@ -81,9 +85,9 @@
this.triggerEnabled = true;
});
if(typeof this.data.next == "function") {
this.data.next().then(data => next);
this.data.next().then(data => nextFn(data));
}else{
this.api.v3.music(this.data.next).then(data => nextFn);
this.api.v3.music(this.data.next).then(data => nextFn(data));
}
}else{
console.log("No next page");

View file

@ -1,51 +1,69 @@
<script type="text/x-template" id="cider-home">
<div class="content-inner home-page">
<div v-if="page == 'main'">
<div class="row">
<div class="col">
<h3>Home</h3>
<div class="well profile-well">
<div class="user-icon">
<mediaitem-artwork shadow="none" :url="profile.attributes.artwork.url" size="300"></mediaitem-artwork>
</div>
<h3>{{ profile.attributes.name }}</h3>
<h4>@{{ profile.attributes.handle }}</h4>
<button class="md-btn">Share</button>
</div>
</div>
<div class="col">
<h3>Recently Played</h3>
<div class="well">
<mediaitem-list-item v-for="item in recentlyPlayed.limit(6)" :item="item"></mediaitem-list-item>
<div class="well artistfeed-well">
<template v-if="isSectionReady('recentlyPlayed')">
<mediaitem-list-item v-for="item in recentlyPlayed.limit(6)"
:item="item"></mediaitem-list-item>
</template>
<div class="spinner" v-else></div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<h3>Your Favorites</h3>
<div class="well">
<mediaitem-scroller-horizontal kind="small" :items="friendsListeningTo" :item="item"></mediaitem-scroller-horizontal>
<div class="row nopadding">
<div class="col nopadding">
<h3>Your Artists Feed</h3>
</div>
<div class="col-auto nopadding flex-center">
<button class="cd-btn-seeall" @click="app.appRoute('artist-feed')">See All</button>
</div>
</div>
<div class="well artistfeed-well" style="margin-top:0px;">
<template v-if="artistFeed.length > 0">
<mediaitem-list-item v-for="item in artistFeed.limit(6)" :item="item"></mediaitem-list-item>
</template>
<template v-else>
<div class="spinner"></div>
</template>
</div>
</div>
</div>
<!-- <div class="row" v-if="app.isDev">-->
<!-- <div class="col">-->
<!-- <h3>Your Favorites</h3>-->
<!-- <div class="well">-->
<!-- <div class="hint-text" v-if="favorites.length == 0">Items you have added to your favorites will-->
<!-- appear here.-->
<!-- </div>-->
<!-- <mediaitem-scroller-horizontal kind="small" :items="favorites"-->
<!-- :item="item"></mediaitem-scroller-horizontal>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<div class="row">
<div class="col">
<h3>Made For You</h3>
<div class="well">
<template v-if="isSectionReady('madeForYou')">
<mediaitem-square kind="small" v-for="item in madeForYou" :item="item"></mediaitem-square>
</div>
</div>
<div class="col">
<h3>Your Artist Feed</h3>
<div class="well">
<mediaitem-list-item v-for="item in recentlyPlayed.limit(6)" :item="item"></mediaitem-list-item>
</template>
<div class="spinner" v-else></div>
</div>
</div>
</div>
<div class="row">
<div class="row" v-if="friendsListeningTo && friendsListeningTo != []">
<div class="col">
<h3>Friends Listening To</h3>
<div class="well">
<mediaitem-square kind="small" v-for="item in friendsListeningTo" :item="item"></mediaitem-square>
<template v-if="isSectionReady('friendsListeningTo')">
<mediaitem-square kind="small" v-for="item in friendsListeningTo"
:item="item"></mediaitem-square>
</template>
<div class="spinner" v-else></div>
</div>
</div>
</div>
</div>
@ -55,28 +73,104 @@
<script>
Vue.component('cider-home', {
template: '#cider-home',
data: function () {
data: function() {
return {
app: this.$root,
followedArtists: this.$root.cfg.home.followedArtists,
favoriteItems: this.$root.cfg.home.favoriteItems,
madeForYou: [],
recentlyPlayed: [],
friendsListeningTo: [],
replayPlaylists: [],
favorites: [],
profile: {},
modify: 0
modify: 0,
artistFeed: [],
showingArtistFeed: false,
page: "main",
sectionsReady: []
}
},
async mounted() {
let self = this
this.getListenNowData()
await this.getArtistFeed()
await this.getFavorites()
},
methods: {
isSectionReady(section) {
return this.sectionsReady.includes(section)
},
removeFavoriteContext() {
let self = this
return {
name: "Remove from Favorites",
action: function(item) {
let index = self.favoriteItems.findIndex(x => x.id == item.id)
if (index > -1) {
self.favoriteItems.splice(index, 1)
self.app.cfg.home.favoriteItems = self.favoriteItems
}
}
}
},
async getFavorites() {
let self = this
let libraryPlaylists = []
let playlists = []
for (let item of this.favoriteItems) {
if (item.type == "library-playlists") {
libraryPlaylists.push(item.id)
} else if (item.type == "playlists") {
playlists.push(item.id)
}
}
if (playlists.length != 0) {
this.app.mk.api.playlists(playlists).then(playlistsData => {
self.favorites.push(...playlistsData)
})
}
if (libraryPlaylists.length != 0) {
this.app.mk.api.library.playlists(libraryPlaylists).then(playlistsData => {
self.favorites.push(...playlistsData)
})
}
},
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 => {
if (item.views["latest-release"].data.length != 0) {
self.artistFeed.push(item.views["latest-release"].data[0])
}
})
// sort artistFeed by attributes.releaseDate descending, date is formatted as "YYYY-MM-DD"
this.artistFeed.sort((a, b) => {
let dateA = new Date(a.attributes.releaseDate)
let dateB = new Date(b.attributes.releaseDate)
return dateB - dateA
})
})
},
getRecentlyPlayed() {
},
async getListenNowData() {
let self = this
this.app.mk.api.personalRecommendations("",
{
this.app.mk.api.personalRecommendations("", {
name: "listen-now",
with: "friendsMix,library,social",
"art[social-profiles:url]": "c",
@ -98,32 +192,34 @@
"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) => {
}).then((data) => {
console.log(data.data[1])
try {
self.madeForYou = data.data.filter(section => {
if (section.meta.metrics.moduleType == "6") {
return section
};
})[0].relationships.contents.data
} catch (err) {}
self.sectionsReady.push("madeForYou")
try {
self.recentlyPlayed = data.data[1].relationships.contents.data
self.friendsListeningTo = data.data.filter(section => {
if (section.meta.metrics.moduleType == "11") {
return section
}
;
})[0].relationships.contents.data
self.madeForYou = data.data.filter(section => {
if (section.meta.metrics.moduleType == "6") {
return section
}
;
};
})[0].relationships.contents.data
} catch (err) {}
self.sectionsReady.push("recentlyPlayed")
self.sectionsReady.push("friendsListeningTo")
});
app.mk.api.v3.music("/v1/me/social/profile/").then((response) => {
self.profile = response.data.data[0]
console.log("!!!")
console.log(response.data.data[0])
})
}

View file

@ -0,0 +1,16 @@
<script type="text/x-template" id="hello-world">
<div class="content-inner">
</div>
</script>
<script>
Vue.component('library-songs', {
template: '#library-songs',
methods: {
sayHello: function () {
alert('Hello world!');
}
}
});
</script>

View file

@ -9,10 +9,10 @@
Audio Quality
</div>
<div class="md-option-segment md-option-segment_auto">
<select class="md-select" style="width:180px;">
<option value="extreme">Extreme</option>
<option value="high">High</option>
<option value="low">Low</option>
<select class="md-select" style="width:180px;" v-model="app.cfg.audio.quality" v-on:change="changeAudioQuality">
<option value="990">Extreme</option>
<option value="256">High</option>
<option value="64">Low</option>
<option value="auto">Auto</option>
</select>
</div>
@ -25,6 +25,34 @@
<input type="checkbox" v-model="app.cfg.audio.seamless_audio" switch/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
Enable Advanced Functionality
<br>
<small>Enabling AudioContext functionality will allow for extended audio features like Audio Normalization , Equalizers and Visualizers, however on some systems this may cause stuttering in audio tracks.</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.advanced.AudioContext" v-on:change="toggleAudioContext" switch/>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext">
<div class="md-option-segment" >
Audio Normalization
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.audio.normalization" v-on:change="toggleNormalization" switch/>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext">
<div class="md-option-segment" >
Audio Spatialization
<br>
<small>Spatialize audio and make audio more 3-dimensional (note: This is not Dolby Atmos)</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.audio.spatial" v-on:change="toggleSpatial" switch/>
</div>
</div>
<div class="md-option-header">
<span>Visual</span>
</div>
@ -54,6 +82,14 @@
</select>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
Animated Window Background
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" switch v-model="app.cfg.visual.bg_artwork_rotation"/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
Window Transparency
@ -72,7 +108,7 @@
<small>(Requires relaunch)</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<select class="md-select" style="width:180px;" v-model="app.cfg.visual.hw_acceleration">
<select class="md-select" style="width:180px;" v-model="app.cfg.visual.hw_acceleration" >
<option value="default">Default</option>
<option value="webgpu">WebGPU</option>
<option value="disabled">Disabled</option>
@ -443,16 +479,6 @@
<div class="md-option-header">
<span>Unfinished / Non Functional</span>
</div>
<div class="md-option-line">
<div class="md-option-segment">
Enable AudioContext Functionality
<br>
<small>Enabling AudioContext functionality will allow for extended audio features like Equalizers and Visualizers, however on some systems this may cause stuttering in audio tracks.</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" switch/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
Theme
@ -547,7 +573,30 @@
}
},
methods: {
toggleAudioContext: function(){
if (app.cfg.advanced.AudioContext){
CiderAudio.init();
if (app.cfg.audio.normalization){
CiderAudio.normalizerOn()}
if (app.cfg.audio.spatial){
CiderAudio.spatialOn()}
} else {
CiderAudio.off();
}
},
toggleNormalization : function(){
if (app.cfg.audio.normalization){
CiderAudio.normalizerOn()
} else {CiderAudio.normalizerOff()}
},
toggleSpatial : function(){
if (app.cfg.audio.spatial){
CiderAudio.spatialOn()
} else {CiderAudio.spatialOff()}
},
changeAudioQuality : function(){
app.mk.bitrate = app.cfg.audio.quality
}
}
})
</script>

View file

@ -2,6 +2,8 @@
<div class="content-inner">
<h3>Welcome to element park. *BERR NERR NERR NERR NERRRRR BERR NER NER NER NERRR BERRR NR NR NRRRR*</h3>
<button @click="app.playMediaItemById('1592151778', 'album')">Play Test Album</button>
<cider-queue ref="queue"></cider-queue>
{{ $store.state.test }}
<div class="spinner"></div>
<button class="md-btn">Cider Button</button>
</div>
</template>

View file

@ -0,0 +1 @@
<svg width="9" height="9" viewBox="0 0 9 9" xmlns="http://www.w3.org/2000/svg" class="glyph" aria-hidden="true"><path d="M1.59 8.991h5.82c1.043 0 1.582-.538 1.582-1.566v-5.85C8.992.547 8.453.008 7.41.008H1.59C.552.008.008.542.008 1.575v5.85c0 1.028.544 1.566 1.582 1.566zm1.812-2.273c-.332 0-.505-.211-.505-.553V2.753c0-.341.173-.553.505-.553h2.264c.245 0 .408.14.408.385 0 .235-.163.384-.408.384H3.854v1.106h1.71c.226 0 .38.125.38.355 0 .221-.154.346-.38.346h-1.71V5.95h1.812c.245 0 .408.14.408.385 0 .235-.163.384-.408.384H3.402z"></path></svg>

After

Width:  |  Height:  |  Size: 546 B

View 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

View 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

6
src/renderer/vuex.min.js vendored Normal file

File diff suppressed because one or more lines are too long