diff --git a/assets/empty.png b/assets/empty.png new file mode 100644 index 00000000..98e0d8b5 Binary files /dev/null and b/assets/empty.png differ diff --git a/assets/icon256.ico b/assets/icon256.ico index fd7bfa43..0dc8f75d 100644 Binary files a/assets/icon256.ico and b/assets/icon256.ico differ diff --git a/assets/icon256.png b/assets/icon256.png index ff52db6b..d08a2b70 100644 Binary files a/assets/icon256.png and b/assets/icon256.png differ diff --git a/assets/icon512.png b/assets/icon512.png new file mode 100644 index 00000000..dd224045 Binary files /dev/null and b/assets/icon512.png differ diff --git a/build/icon.png b/build/icon.png index ff52db6b..dd224045 100644 Binary files a/build/icon.png and b/build/icon.png differ diff --git a/build/installerIcon.png b/build/installerIcon.png index ff52db6b..d08a2b70 100644 Binary files a/build/installerIcon.png and b/build/installerIcon.png differ diff --git a/build/uninstallerIcon.png b/build/uninstallerIcon.png index ff52db6b..d08a2b70 100644 Binary files a/build/uninstallerIcon.png and b/build/uninstallerIcon.png differ diff --git a/common/intl/translations/de.json b/common/intl/translations/de.json index e075d910..c693ad74 100644 --- a/common/intl/translations/de.json +++ b/common/intl/translations/de.json @@ -19,7 +19,7 @@ "setting.title.position": "Position", "setting.title.theme": "Theme", - "setting.title.general": "General", + "setting.title.general": "Allgemein", "setting.title.game-overlay": "Spiel-Overlay", "setting.title.about": "Informationen", diff --git a/common/intl/translations/ja.json b/common/intl/translations/ja.json index f6d8f527..4be96ff4 100644 --- a/common/intl/translations/ja.json +++ b/common/intl/translations/ja.json @@ -19,7 +19,7 @@ "setting.title.position": "位置", "setting.title.theme": "テーマ", - "setting.title.general": "General", + "setting.title.general": "一般", "setting.title.game-overlay": "ゲームオーバーレイ", "setting.title.about": "情報", diff --git a/extensions/alspotron.js b/extensions/alspotron.js index 706d15ce..08508012 100644 --- a/extensions/alspotron.js +++ b/extensions/alspotron.js @@ -2,7 +2,6 @@ // AUTHOR: Khinenw // DESCRIPTION: Get current playing information to show in Alspotron - (function Alspotron() { const LyricResolvers = { v2(uri) { @@ -37,12 +36,18 @@ } const uri = Spicetify.Player.data.track.uri; + let imageUrl = Spicetify.Player.data.track.metadata.image_xlarge_url; + if (imageUrl?.indexOf('localfile') === -1) { + imageUrl = `https://i.scdn.co/image/${imageUrl.substring(imageUrl.lastIndexOf(":") + 1)}`; + } + if (previousInfo.uri !== uri) { return { status: 'playing', title: Spicetify.Player.data.track.metadata.title, artists: [Spicetify.Player.data.track.metadata.artist_name], - cover_url: uri, + cover_url: imageUrl, + uri: uri, duration: Spicetify.Player.getDuration(), progress: Spicetify.Player.getProgress(), lyrics: await getLyric() @@ -51,7 +56,8 @@ return { status: 'playing', - cover_url: uri, + uri: uri, + cover_url: imageUrl, duration: Spicetify.Player.getDuration(), progress: Spicetify.Player.getProgress() }; diff --git a/extensions/spicetify.d.ts b/extensions/spicetify.d.ts new file mode 100644 index 00000000..f410d4ff --- /dev/null +++ b/extensions/spicetify.d.ts @@ -0,0 +1,2100 @@ +// Type definitions for Spicetify +/* eslint-disable */ +import React from 'react'; + +declare namespace Spicetify { + type Icon = 'album' | 'artist' | 'block' | 'brightness' | 'car' | 'chart-down' | 'chart-up' | 'check' | 'check-alt-fill' | 'chevron-left' | 'chevron-right' | 'chromecast-disconnected' | 'clock' | 'collaborative' | 'computer' | 'copy' | 'download' | 'downloaded' | 'edit' | 'enhance' | 'exclamation-circle' | 'external-link' | 'facebook' | 'follow' | 'fullscreen' | 'gamepad' | 'grid-view' | 'heart' | 'heart-active' | 'instagram' | 'laptop' | 'library' | 'list-view' | 'location' | 'locked' | 'locked-active' | 'lyrics' | 'menu' | 'minimize' | 'minus' | 'more' | 'new-spotify-connect' | 'offline' | 'pause' | 'phone' | 'play' | 'playlist' | 'playlist-folder' | 'plus-alt' | 'plus2px' | 'podcasts' | 'projector' | 'queue' | 'repeat' | 'repeat-once' | 'search' | 'search-active' | 'shuffle' | 'skip-back' | 'skip-back15' | 'skip-forward' | 'skip-forward15' | 'soundbetter' | 'speaker' | 'spotify' | 'subtitles' | 'tablet' | 'ticket' | 'twitter' | 'visualizer' | 'voice' | 'volume' | 'volume-off' | 'volume-one-wave' | 'volume-two-wave' | 'watch' | 'x'; + type Variant = 'bass' | 'forte' | 'brio' | 'altoBrio' | 'alto' | 'canon' | 'celloCanon' | 'cello' | 'ballad' | 'balladBold' | 'viola' | 'violaBold' | 'mesto' | 'mestoBold' | 'metronome' | 'finale' | 'finaleBold' | 'minuet' | 'minuetBold'; + type SemanticColor = 'textBase' | 'textSubdued' | 'textBrightAccent' | 'textNegative' | 'textWarning' | 'textPositive' | 'textAnnouncement' | 'essentialBase' | 'essentialSubdued' | 'essentialBrightAccent' | 'essentialNegative' | 'essentialWarning' | 'essentialPositive' | 'essentialAnnouncement' | 'decorativeBase' | 'decorativeSubdued' | 'backgroundBase' | 'backgroundHighlight' | 'backgroundPress' | 'backgroundElevatedBase' | 'backgroundElevatedHighlight' | 'backgroundElevatedPress' | 'backgroundTintedBase' | 'backgroundTintedHighlight' | 'backgroundTintedPress' | 'backgroundUnsafeForSmallTextBase' | 'backgroundUnsafeForSmallTextHighlight' | 'backgroundUnsafeForSmallTextPress'; + type Metadata = Partial>; + type ContextTrack = { + uri: string; + uid?: string; + metadata?: Metadata; + }; + type ProvidedTrack = ContextTrack & { + removed?: string[]; + blocked?: string[]; + provider?: string; + }; + type ContextOption = { + contextURI?: string; + index?: number; + trackUri?: string; + page?: number; + trackUid?: string; + sortedBy?: string; + filteredBy?: string; + shuffleContext?: boolean; + repeatContext?: boolean; + repeatTrack?: boolean; + offset?: number; + next_page_url?: string; + restrictions?: Record; + referrer?: string; + }; + type PlayerState = { + timestamp: number; + context_uri: string; + context_url: string; + context_restrictions: Record; + index?: { + page: number; + track: number; + }; + track?: ProvidedTrack; + playback_id?: string; + playback_quality?: string; + playback_speed?: number; + position_as_of_timestamp: number; + duration: number; + is_playing: boolean; + is_paused: boolean; + is_buffering: boolean; + play_origin: { + feature_identifier: string; + feature_version: string; + view_uri?: string; + external_referrer?: string; + referrer_identifier?: string; + device_identifier?: string; + }; + options: { + shuffling_context?: boolean; + repeating_context?: boolean; + repeating_track?: boolean; + }; + restrictions: Record; + suppressions: { + providers: string[]; + }; + debug: { + log: string[]; + }; + prev_tracks: ProvidedTrack[]; + next_tracks: ProvidedTrack[]; + context_metadata: Metadata; + page_metadata: Metadata; + session_id: string; + queue_revision: string; + }; + namespace Player { + /** + * Register a listener `type` on Spicetify.Player. + * + * On default, `Spicetify.Player` always dispatch: + * - `songchange` type when player changes track. + * - `onplaypause` type when player plays or pauses. + * - `onprogress` type when track progress changes. + * - `appchange` type when user changes page. + */ + function addEventListener(type: string, callback: (event?: Event) => void): void; + function addEventListener(type: 'songchange', callback: (event?: Event & { data: PlayerState }) => void): void; + function addEventListener(type: 'onplaypause', callback: (event?: Event & { data: PlayerState }) => void): void; + function addEventListener(type: 'onprogress', callback: (event?: Event & { data: number }) => void): void; + function addEventListener(type: 'appchange', callback: (event?: Event & { data: { + /** + * App href path + */ + path: string; + /** + * App container + */ + container: HTMLElement; + } }) => void): void; + /** + * Skip to previous track. + */ + function back(): void; + /** + * An object contains all information about current track and player. + */ + const data: PlayerState; + /** + * Decrease a small amount of volume. + */ + function decreaseVolume(): void; + /** + * Dispatches an event at `Spicetify.Player`. + * + * On default, `Spicetify.Player` always dispatch + * - `songchange` type when player changes track. + * - `onplaypause` type when player plays or pauses. + * - `onprogress` type when track progress changes. + * - `appchange` type when user changes page. + */ + function dispatchEvent(event: Event): void; + const eventListeners: { + [key: string]: Array<(event?: Event) => void> + }; + /** + * Convert milisecond to `mm:ss` format + * @param milisecond + */ + function formatTime(milisecond: number): string; + /** + * Return song total duration in milisecond. + */ + function getDuration(): number; + /** + * Return mute state + */ + function getMute(): boolean; + /** + * Return elapsed duration in milisecond. + */ + function getProgress(): number; + /** + * Return elapsed duration in percentage (0 to 1). + */ + function getProgressPercent(): number; + /** + * Return current Repeat state (No repeat = 0/Repeat all = 1/Repeat one = 2). + */ + function getRepeat(): number; + /** + * Return current shuffle state. + */ + function getShuffle(): boolean; + /** + * Return track heart state. + */ + function getHeart(): boolean; + /** + * Return current volume level (0 to 1). + */ + function getVolume(): number; + /** + * Increase a small amount of volume. + */ + function increaseVolume(): void; + /** + * Return a boolean whether player is playing. + */ + function isPlaying(): boolean; + /** + * Skip to next track. + */ + function next(): void; + /** + * Pause track. + */ + function pause(): void; + /** + * Resume track. + */ + function play(): void; + /** + * Play a track, playlist, album, etc. immediately + * @param uri Spotify URI + * @param context + * @param options + */ + function playUri(uri: string, context?: any, options?: any): Promise; + /** + * Unregister added event listener `type`. + * @param type + * @param callback + */ + function removeEventListener(type: string, callback: (event?: Event) => void): void; + /** + * Seek track to position. + * @param position can be in percentage (0 to 1) or in milisecond. + */ + function seek(position: number): void; + /** + * Turn mute on/off + * @param state + */ + function setMute(state: boolean): void; + /** + * Change Repeat mode + * @param mode `0` No repeat. `1` Repeat all. `2` Repeat one track. + */ + function setRepeat(mode: number): void; + /** + * Turn shuffle on/off. + * @param state + */ + function setShuffle(state: boolean): void; + /** + * Set volume level + * @param level 0 to 1 + */ + function setVolume(level: number): void; + /** + * Seek to previous `amount` of milisecond + * @param amount in milisecond. Default: 15000. + */ + function skipBack(amount?: number): void; + /** + * Seek to next `amount` of milisecond + * @param amount in milisecond. Default: 15000. + */ + function skipForward(amount?: number): void; + /** + * Toggle Heart (Favourite) track state. + */ + function toggleHeart(): void; + /** + * Toggle Mute/No mute. + */ + function toggleMute(): void; + /** + * Toggle Play/Pause. + */ + function togglePlay(): void; + /** + * Toggle No repeat/Repeat all/Repeat one. + */ + function toggleRepeat(): void; + /** + * Toggle Shuffle/No shuffle. + */ + function toggleShuffle(): void; + } + /** + * Adds a track or array of tracks to prioritized queue. + */ + function addToQueue(uri: ContextTrack[]): Promise; + /** + * @deprecated + */ + const BridgeAPI: any; + /** + * @deprecated + */ + const CosmosAPI: any; + /** + * Async wrappers of CosmosAPI + */ + namespace CosmosAsync { + type Method = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'SUB'; + interface Error { + code: number; + error: string; + message: string; + stack?: string; + } + + type Headers = Record; + type Body = Record; + + interface Response { + body: any; + headers: Headers; + status: number; + uri?: string; + } + + function head(url: string, headers?: Headers): Promise; + function get(url: string, body?: Body, headers?: Headers): Promise; + function post(url: string, body?: Body, headers?: Headers): Promise; + function put(url: string, body?: Body, headers?: Headers): Promise; + function del(url: string, body?: Body, headers?: Headers): Promise; + function patch(url: string, body?: Body, headers?: Headers): Promise; + function sub(url: string, callback: ((b: Response['body']) => void), onError?: ((e: Error) => void), body?: Body, headers?: Headers): Promise; + function postSub(url: string, body: Body | null, callback: ((b: Response['body']) => void), onError?: ((e: Error) => void)): Promise; + function request(method: Method, url: string, body?: Body, headers?: Headers): Promise; + function resolve(method: Method, url: string, body?: Body, headers?: Headers): Promise; + } + /** + * Fetch interesting colors from URI. + * @param uri Any type of URI that has artwork (playlist, track, album, artist, show, ...) + */ + function colorExtractor(uri: string): Promise<{ + DESATURATED: string; + LIGHT_VIBRANT: string; + PROMINENT: string; + VIBRANT: string; + VIBRANT_NON_ALARMING: string; + }>; + /** + * @deprecated + */ + function getAblumArtColors(): any; + /** + * Fetch track analyzed audio data. + * Beware, not all tracks have audio data. + * @param uri is optional. Leave it blank to get current track + * or specify another track uri. + */ + function getAudioData(uri?: string): Promise; + /** + * Set of APIs method to register, deregister hotkeys/shortcuts + */ + namespace Keyboard { + type ValidKey = 'BACKSPACE' | 'TAB' | 'ENTER' | 'SHIFT' | 'CTRL' | 'ALT' | 'CAPS' | 'ESCAPE' | 'SPACE' | 'PAGE_UP' | 'PAGE_DOWN' | 'END' | 'HOME' | 'ARROW_LEFT' | 'ARROW_UP' | 'ARROW_RIGHT' | 'ARROW_DOWN' | 'INSERT' | 'DELETE' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | 'WINDOW_LEFT' | 'WINDOW_RIGHT' | 'SELECT' | 'NUMPAD_0' | 'NUMPAD_1' | 'NUMPAD_2' | 'NUMPAD_3' | 'NUMPAD_4' | 'NUMPAD_5' | 'NUMPAD_6' | 'NUMPAD_7' | 'NUMPAD_8' | 'NUMPAD_9' | 'MULTIPLY' | 'ADD' | 'SUBTRACT' | 'DECIMAL_POINT' | 'DIVIDE' | 'F1' | 'F2' | 'F3' | 'F4' | 'F5' | 'F6' | 'F7' | 'F8' | 'F9' | 'F10' | 'F11' | 'F12' | ';' | '=' | ' | ' | '-' | '.' | '/' | '`' | '[' | '\\' | ']' | '"' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')' | '_' | '+' | ':' | '<' | '>' | '?' | '|'; + type KeysDefine = string | { + key: string; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; + meta?: boolean; + }; + const KEYS: Record; + function registerShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; + function registerIsolatedShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; + function registerImportantShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; + function _deregisterShortcut(keys: KeysDefine): void; + function deregisterImportantShortcut(keys: KeysDefine): void; + function changeShortcut(keys: KeysDefine, newKeys: KeysDefine): void; + } + + /** + * @deprecated + */ + const LiveAPI: any; + + namespace LocalStorage { + /** + * Empties the list associated with the object of all key/value pairs, if there are any. + */ + function clear(): void; + /** + * Get key value + */ + function get(key: string): string | null; + /** + * Delete key + */ + function remove(key: string): void; + /** + * Set new value for key + */ + function set(key: string, value: string): void; + } + /** + * To create and prepend custom menu item in profile menu. + */ + namespace Menu { + /** + * Create a single toggle. + */ + class Item { + constructor(name: string, isEnabled: boolean, onClick: (self: Item) => void, icon?: Icon | string); + name: string; + isEnabled: boolean; + /** + * Change item name + */ + setName(name: string): void; + /** + * Change item enabled state. + * Visually, item would has a tick next to it if its state is enabled. + */ + setState(isEnabled: boolean): void; + /** + * Change icon + */ + setIcon(icon: Icon | string): void; + /** + * Item is only available in Profile menu when method "register" is called. + */ + register(): void; + /** + * Stop item to be prepended into Profile menu. + */ + deregister(): void; + } + + /** + * Create a sub menu to contain Item toggles. + * `Item`s in `subItems` array shouldn't be registered. + */ + class SubMenu { + constructor(name: string, subItems: Item[]); + name: string; + /** + * Change SubMenu name + */ + setName(name: string): void; + /** + * Add an item to sub items list + */ + addItem(item: Item): any; + /** + * Remove an item from sub items list + */ + removeItem(item: Item): any; + /** + * SubMenu is only available in Profile menu when method "register" is called. + */ + register(): void; + /** + * Stop SubMenu to be prepended into Profile menu. + */ + deregister(): void; + } + } + + /** + * Keyboard shortcut library + * + * Documentation: https://craig.is/killing/mice v1.6.5 + * + * Spicetify.Keyboard is wrapper of this library to be compatible with legacy Spotify, + * so new extension should use this library instead. + */ + function Mousetrap(element?: any): void; + + /** + * Contains vast array of internal APIs. + * Please explore in Devtool Console. + */ + const Platform: any; + /** + * Queue object contains list of queuing tracks, + * history of played tracks and current track metadata. + */ + const Queue: { + nextTracks: any[]; + prevTracks: any[]; + queueRevision: string; + track: any; + }; + /** + * Remove a track or array of tracks from current queue. + */ + function removeFromQueue(uri: ContextTrack[]): Promise; + /** + * Display a bubble of notification. Useful for a visual feedback. + * @param message Message to display. Can use inline HTML for styling. + * @param isError If true, bubble will be red. Defaults to false. + * @param msTimeout Time in milliseconds to display the bubble. Defaults to Spotify's value. + */ + function showNotification(message: React.ReactNode, isError?: boolean, msTimeout?: number): void; + /** + * Set of APIs method to parse and validate URIs. + */ + class URI { + constructor(type: string, props: any); + public type: string; + public hasBase62Id: boolean; + + public id?: string; + public disc?: any; + public args?: any; + public category?: string; + public username?: string; + public track?: string; + public artist?: string; + public album?: string; + public duration?: number; + public query?: string; + public country?: string; + public global?: boolean; + public context?: string | typeof URI | null; + public anchor?: string; + public play?: any; + public toplist?: any; + + /** + * + * @return The URI representation of this uri. + */ + toURI(): string; + + /** + * + * @return The URI representation of this uri. + */ + toString(): string; + + /** + * Get the URL path of this uri. + * + * @param opt_leadingSlash True if a leading slash should be prepended. + * @return The path of this uri. + */ + toURLPath(opt_leadingSlash: boolean): string; + + /** + * + * @param origin The origin to use for the URL. + * @return The URL string for the uri. + */ + toURL(origin?: string): string; + + /** + * Clones a given SpotifyURI instance. + * + * @return An instance of URI. + */ + clone(): URI | null; + + /** + * Gets the path of the URI object by removing all hash and query parameters. + * + * @return The path of the URI object. + */ + getPath(): string; + + /** + * The various URI Types. + * + * Note that some of the types in this enum are not real URI types, but are + * actually URI particles. They are marked so. + * + */ + static Type: { + AD: string; + ALBUM: string; + GENRE: string; + QUEUE: string; + APPLICATION: string; + ARTIST: string; + ARTIST_TOPLIST: string; + ARTIST_CONCERTS: string; + AUDIO_FILE: string; + COLLECTION: string; + COLLECTION_ALBUM: string; + COLLECTION_ARTIST: string; + COLLECTION_MISSING_ALBUM: string; + COLLECTION_TRACK_LIST: string; + CONCERT: string; + CONTEXT_GROUP: string; + DAILY_MIX: string; + EMPTY: string; + EPISODE: string; + /** URI particle; not an actual URI. */ + FACEBOOK: string; + FOLDER: string; + FOLLOWERS: string; + FOLLOWING: string; + IMAGE: string; + INBOX: string; + INTERRUPTION: string; + LIBRARY: string; + LIVE: string; + ROOM: string; + EXPRESSION: string; + LOCAL: string; + LOCAL_TRACK: string; + LOCAL_ALBUM: string; + LOCAL_ARTIST: string; + MERCH: string; + MOSAIC: string; + PLAYLIST: string; + PLAYLIST_V2: string; + PRERELEASE: string; + PROFILE: string; + PUBLISHED_ROOTLIST: string; + RADIO: string; + ROOTLIST: string; + SEARCH: string; + SHOW: string; + SOCIAL_SESSION: string; + SPECIAL: string; + STARRED: string; + STATION: string; + TEMP_PLAYLIST: string; + TOPLIST: string; + TRACK: string; + TRACKSET: string; + USER_TOPLIST: string; + USER_TOP_TRACKS: string; + UNKNOWN: string; + MEDIA: string; + QUESTION: string; + POLL: string; + }; + + /** + * Creates a new URI object from a parsed string argument. + * + * @param str The string that will be parsed into a URI object. + * @throws TypeError If the string argument is not a valid URI, a TypeError will + * be thrown. + * @return The parsed URI object. + */ + static fromString(str: string): URI; + + /** + * Parses a given object into a URI instance. + * + * Unlike URI.fromString, this function could receive any kind of value. If + * the value is already a URI instance, it is simply returned. + * Otherwise the value will be stringified before parsing. + * + * This function also does not throw an error like URI.fromString, but + * instead simply returns null if it can't parse the value. + * + * @param value The value to parse. + * @return The corresponding URI instance, or null if the + * passed value is not a valid value. + */ + static from(value: any): URI | null; + + /** + * Checks whether two URI:s refer to the same thing even though they might + * not necessarily be equal. + * + * These two Playlist URIs, for example, refer to the same playlist: + * + * spotify:user:napstersean:playlist:3vxotOnOGDlZXyzJPLFnm2 + * spotify:playlist:3vxotOnOGDlZXyzJPLFnm2 + * + * @param baseUri The first URI to compare. + * @param refUri The second URI to compare. + * @return Whether they shared idenitity + */ + static isSameIdentity(baseUri: URI | string, refUri: URI | string): boolean; + + /** + * Returns the hex representation of a Base62 encoded id. + * + * @param id The base62 encoded id. + * @return The hex representation of the base62 id. + */ + static idToHex(id: string): string; + + /** + * Returns the base62 representation of a hex encoded id. + * + * @param hex The hex encoded id. + * @return The base62 representation of the id. + */ + static hexToId(hex: string): string; + + /** + * Creates a new 'album' type URI. + * + * @param id The id of the album. + * @param disc The disc number of the album. + * @return The album URI. + */ + static albumURI(id: string, disc: number): URI; + + /** + * Creates a new 'application' type URI. + * + * @param id The id of the application. + * @param args An array containing the arguments to the app. + * @return The application URI. + */ + static applicationURI(id: string, args: string[]): URI; + + /** + * Creates a new 'artist' type URI. + * + * @param id The id of the artist. + * @return The artist URI. + */ + static artistURI(id: string): URI; + + /** + * Creates a new 'collection' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param category The category of the collection. + * @return The collection URI. + */ + static collectionURI(username: string, category: string): URI; + + /** + * Creates a new 'collection-album' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param id The id of the album. + * @return The collection album URI. + */ + static collectionAlbumURI(username: string, id: string): URI; + + /** + * Creates a new 'collection-artist' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param id The id of the artist. + * @return The collection artist URI. + */ + static collectionAlbumURI(username: string, id: string): URI; + + /** + * Creates a new 'concert' type URI. + * + * @param id The id of the concert. + * @return The concert URI. + */ + static concertURI(id: string): URI; + + /** + * Creates a new 'episode' type URI. + * + * @param id The id of the episode. + * @return The episode URI. + */ + static episodeURI(id: string): URI; + + /** + * Creates a new 'folder' type URI. + * + * @param id The id of the folder. + * @return The folder URI. + */ + static folderURI(id: string): URI; + + /** + * Creates a new 'local-album' type URI. + * + * @param artist The artist of the album. + * @param album The name of the album. + * @return The local album URI. + */ + static localAlbumURI(artist: string, album: string): URI; + + /** + * Creates a new 'local-artist' type URI. + * + * @param artist The name of the artist. + * @return The local artist URI. + */ + static localArtistURI(artist: string): URI; + + /** + * Creates a new 'playlist-v2' type URI. + * + * @param id The id of the playlist. + * @return The playlist URI. + */ + static playlistV2URI(id: string): URI; + + /** + * Creates a new 'prerelease' type URI. + * + * @param id The id of the prerelease. + * @return The prerelease URI. + */ + static prereleaseURI(id: string): URI; + + /** + * Creates a new 'profile' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param args A list of arguments. + * @return The profile URI. + */ + static profileURI(username: string, args: string[]): URI; + + /** + * Creates a new 'search' type URI. + * + * @param query The unencoded search query. + * @return The search URI + */ + static searchURI(query: string): URI; + + /** + * Creates a new 'show' type URI. + * + * @param id The id of the show. + * @return The show URI. + */ + static showURI(id: string): URI; + + /** + * Creates a new 'station' type URI. + * + * @param args An array of arguments for the station. + * @return The station URI. + */ + static stationURI(args: string[]): URI; + + /** + * Creates a new 'track' type URI. + * + * @param id The id of the track. + * @param anchor The point in the track formatted as mm:ss + * @param context An optional context URI + * @param play Toggles autoplay + * @return The track URI. + */ + static trackURI(id: string, anchor: string, context?: string, play?: boolean): URI; + + /** + * Creates a new 'user-toplist' type URI. + * + * @param username The non-canonical username of the toplist owner. + * @param toplist The toplist type. + * @return The user-toplist URI. + */ + static userToplistURI(username: string, toplist: string): URI; + + static isAd(uri: URI | string): boolean; + static isAlbum(uri: URI | string): boolean; + static isGenre(uri: URI | string): boolean; + static isQueue(uri: URI | string): boolean; + static isApplication(uri: URI | string): boolean; + static isArtist(uri: URI | string): boolean; + static isArtistToplist(uri: URI | string): boolean; + static isArtistConcerts(uri: URI | string): boolean; + static isAudioFile(uri: URI | string): boolean; + static isCollection(uri: URI | string): boolean; + static isCollectionAlbum(uri: URI | string): boolean; + static isCollectionArtist(uri: URI | string): boolean; + static isCollectionMissingAlbum(uri: URI | string): boolean; + static isCollectionTrackList(uri: URI | string): boolean; + static isConcert(uri: URI | string): boolean; + static isContextGroup(uri: URI | string): boolean; + static isDailyMix(uri: URI | string): boolean; + static isEmpty(uri: URI | string): boolean; + static isEpisode(uri: URI | string): boolean; + static isFacebook(uri: URI | string): boolean; + static isFolder(uri: URI | string): boolean; + static isFollowers(uri: URI | string): boolean; + static isFollowing(uri: URI | string): boolean; + static isImage(uri: URI | string): boolean; + static isInbox(uri: URI | string): boolean; + static isInterruption(uri: URI | string): boolean; + static isLibrary(uri: URI | string): boolean; + static isLive(uri: URI | string): boolean; + static isRoom(uri: URI | string): boolean; + static isExpression(uri: URI | string): boolean; + static isLocal(uri: URI | string): boolean; + static isLocalTrack(uri: URI | string): boolean; + static isLocalAlbum(uri: URI | string): boolean; + static isLocalArtist(uri: URI | string): boolean; + static isMerch(uri: URI | string): boolean; + static isMosaic(uri: URI | string): boolean; + static isPlaylist(uri: URI | string): boolean; + static isPlaylistV2(uri: URI | string): boolean; + static isPrerelease(uri: URI | string): boolean; + static isProfile(uri: URI | string): boolean; + static isPublishedRootlist(uri: URI | string): boolean; + static isRadio(uri: URI | string): boolean; + static isRootlist(uri: URI | string): boolean; + static isSearch(uri: URI | string): boolean; + static isShow(uri: URI | string): boolean; + static isSocialSession(uri: URI | string): boolean; + static isSpecial(uri: URI | string): boolean; + static isStarred(uri: URI | string): boolean; + static isStation(uri: URI | string): boolean; + static isTempPlaylist(uri: URI | string): boolean; + static isToplist(uri: URI | string): boolean; + static isTrack(uri: URI | string): boolean; + static isTrackset(uri: URI | string): boolean; + static isUserToplist(uri: URI | string): boolean; + static isUserTopTracks(uri: URI | string): boolean; + static isUnknown(uri: URI | string): boolean; + static isMedia(uri: URI | string): boolean; + static isQuestion(uri: URI | string): boolean; + static isPoll(uri: URI | string): boolean; + static isPlaylistV1OrV2(uri: URI | string): boolean; + } + + /** + * Create custom menu item and prepend to right click context menu + */ + namespace ContextMenu { + type OnClickCallback = (uris: string[], uids?: string[], contextUri?: string) => void; + type ShouldAddCallback = (uris: string[], uids?: string[], contextUri?: string) => boolean; + + // Single context menu item + class Item { + /** + * List of valid icons to use. + */ + static readonly iconList: Icon[]; + constructor(name: string, onClick: OnClickCallback, shouldAdd?: ShouldAddCallback, icon?: Icon, disabled?: boolean); + name: string; + icon: Icon | string; + disabled: boolean; + /** + * A function returning boolean determines whether item should be prepended. + */ + shouldAdd: ShouldAddCallback; + /** + * A function to call when item is clicked + */ + onClick: OnClickCallback; + /** + * Item is only available in Context Menu when method "register" is called. + */ + register: () => void; + /** + * Stop Item to be prepended into Context Menu. + */ + deregister: () => void; + } + + /** + * Create a sub menu to contain `Item`s. + * `Item`s in `subItems` array shouldn't be registered. + */ + class SubMenu { + constructor(name: string, subItems: Iterable, shouldAdd?: ShouldAddCallback, disabled?: boolean); + name: string; + disabled: boolean; + /** + * A function returning boolean determines whether item should be prepended. + */ + shouldAdd: ShouldAddCallback; + addItem: (item: Item) => void; + removeItem: (item: Item) => void; + /** + * SubMenu is only available in Context Menu when method "register" is called. + */ + register: () => void; + /** + * Stop SubMenu to be prepended into Context Menu. + */ + deregister: () => void; + } + } + + /** + * Popup Modal + */ + namespace PopupModal { + interface Content { + title: string; + /** + * You can specify a string for simple text display + * or a HTML element for interactive config/setting menu + */ + content: string | Element; + /** + * Bigger window + */ + isLarge?: boolean; + } + + function display(e: Content): void; + function hide(): void; + } + + /** React instance to create components */ + const React: any; + /** React DOM instance to render and mount components */ + const ReactDOM: any; + /** React DOM Server instance to render components to string */ + const ReactDOMServer: any; + + /** Stock React components exposed from Spotify library */ + namespace ReactComponent { + type ContextMenuProps = { + /** + * Decide whether to use the global singleton context menu (rendered in ) + * or a new inline context menu (rendered in a sibling + * element to `children`) + */ + renderInline?: boolean; + /** + * Determins what will trigger the context menu. For example, a click, or a right-click + */ + trigger?: 'click' | 'right-click'; + /** + * Determins is the context menu should open or toggle when triggered + */ + action?: 'toggle' | 'open'; + /** + * The preferred placement of the context menu when it opens. + * Relative to trigger element. + */ + placement?: 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end'; + /** + * The x and y offset distances at which the context menu should open. + * Relative to trigger element and `position`. + */ + offset?: [number, number]; + /** + * Will stop the client from scrolling while the context menu is open + */ + preventScrollingWhileOpen?: boolean; + /** + * The menu UI to render inside of the context menu. + */ + menu: typeof Spicetify.ReactComponent.Menu + + + + ; + /** + * A child of the context menu. Should be ` + ); +}; + +const TitleBar = () => { + const [isMaximized, setMaximized] = createSignal(false); + + return ( +
+ +
+ + + + + +
+ ) +}; + +export default TitleBar; diff --git a/renderer/components/Titlebar.tsx b/renderer/components/Titlebar.tsx deleted file mode 100644 index 80c941bc..00000000 --- a/renderer/components/Titlebar.tsx +++ /dev/null @@ -1,69 +0,0 @@ -// import { BrowserWindow } from 'electron' -import { Show } from 'solid-js'; - -import { cx } from '../utils/classNames'; - -import type { JSX } from 'solid-js/jsx-runtime'; - -const isMac = /Mac/.test(navigator.userAgent); - -type ButtonProps = { - onClick?: () => void; - children: JSX.Element; -} - -const Button = (props: ButtonProps) => { - return ( - - ) -} - -const Titlebar = () => { - return ( -
- -
- - - -
-
-
- ) -}; - -export default Titlebar; diff --git a/renderer/lyrics/App.tsx b/renderer/lyrics/App.tsx index c137f81c..5854dfd4 100644 --- a/renderer/lyrics/App.tsx +++ b/renderer/lyrics/App.tsx @@ -72,7 +72,7 @@ const LyricsMapEditor = () => { class={` w-full h-full flex flex-row justify-start items-stretch gap-0 - text-white + text-black dark:text-white `} > @@ -100,7 +100,10 @@ const LyricsMapEditor = () => { /> @@ -109,7 +112,7 @@ const LyricsMapEditor = () => { -
+
@@ -117,7 +120,7 @@ const LyricsMapEditor = () => { {(metadata) => ( onSelect(metadata)}>
-
+
ID: {metadata.lyricId}
@@ -129,7 +132,7 @@ const LyricsMapEditor = () => {
-
+
{metadata.registerDate ? new Date(metadata.registerDate).toLocaleString(undefined, { timeZone: 'Asia/Seoul', hour12: false, @@ -138,13 +141,16 @@ const LyricsMapEditor = () => { }) : 'Invalid Date'}
= 0}> -
+
: {formatTime(metadata.playtime)}
- + )} diff --git a/renderer/lyrics/SideBar.tsx b/renderer/lyrics/SideBar.tsx index d2706aa0..9fe2fece 100644 --- a/renderer/lyrics/SideBar.tsx +++ b/renderer/lyrics/SideBar.tsx @@ -47,7 +47,7 @@ const SideBar = () => { class={` w-[312px] h-full p-4 flex flex-col justify-start items-stretch gap-2 - text-white + text-black dark:text-white `} >
@@ -74,7 +74,7 @@ const SideBar = () => {
-
+
: {alsongLyric()?.lyricId ?? 'N/A'} {' · '} : {alsongLyric()?.register?.name ?? 'N/A'} diff --git a/renderer/settings/App.tsx b/renderer/settings/App.tsx index feacaf99..ff3e6233 100644 --- a/renderer/settings/App.tsx +++ b/renderer/settings/App.tsx @@ -1,8 +1,8 @@ -import { Match, Switch, createSignal } from 'solid-js'; +import { For, JSX, Setter } from 'solid-js'; import { Transition } from 'solid-transition-group'; -import { TransProvider, useTransContext } from '@jellybrick/solid-i18next'; +import { useTransContext } from '@jellybrick/solid-i18next'; -import { TFunction } from 'i18next'; +import { Navigate, Route, Routes, useLocation, useNavigate } from '@solidjs/router'; import ListView, { ListItemData } from './components/ListView'; @@ -12,137 +12,132 @@ import GeneralContainer from './containers/GeneralContainer'; import PositionContainer from './containers/PositionContainer'; import ThemeContainer from './containers/ThemeContainer'; -import Layout from '../components/Layout'; -import { LangResource } from '../../common/intl'; -import useConfig from '../hooks/useConfig'; +import GameListContainer from './containers/GameListContainer'; +import Layout from '../components/Layout'; -const getTabList = (t: TFunction) => { +export interface TabItemData extends Omit { + container: () => JSX.Element; +} - const TAB_LIST: ListItemData[] = [ - // { - // id: 'plugin', - // label: '플러그인', - // }, - ]; +const TAB_LIST = (() => { + const result: TabItemData[] = []; - TAB_LIST.push( + result.push( { id: 'general', - label: t('setting.title.general'), icon: ( - ) + ), + container: GeneralContainer, }, { id: 'position', - label: t('setting.title.position'), icon: ( + class={'fill-black dark:fill-white'} + /> - ) + ), + container: PositionContainer, }, { id: 'theme', - label: t('setting.title.theme'), icon: ( + class={'fill-black dark:fill-white'} + /> - ) + ), + container: ThemeContainer, }, ); if (window.isWindows) { - TAB_LIST.push({ - id: 'game', - label: t('setting.title.game-overlay'), + result.push({ + id: 'game-overlay', icon: ( + class={'fill-black dark:fill-white'} + /> - ) + ), + container: GameContainer }); } - TAB_LIST.push({ + result.push({ id: 'about', - label: t('setting.title.about'), icon: ( + class={'fill-black dark:fill-white'} + /> - ) + ), + container: InfoContainer, }); - return TAB_LIST; - -}; + return result; +})(); const App = () => { - const [tabId, setTabId] = createSignal('general'); + + const [t] = useTransContext(); + const navigate = useNavigate(); + const location = useLocation(); + + /* properties */ + const tabId = () => location.pathname.match(/(?<=\/)[^/]+/)?.[0] ?? ''; + const listItem = () => TAB_LIST.map((item) => ({ + ...item, + label: t(`setting.title.${item.id}`), + })); + + /* methods */ + const setTabId = ((id: string) => { + navigate(`/${id}`); + }) as Setter; return ( - - -
- { - const [t] = useTransContext(); - return getTabList(t); - })()} - /> - - - - - - - - - - - - - - - - - - -
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. -
-
-
-
-
-
-
- ) + +
+ setTabId(tab.id)} + /> + + + + {(tab) => } + + + } /> + + +
+
+ ); }; export default App; diff --git a/renderer/settings/Provider.tsx b/renderer/settings/Provider.tsx new file mode 100644 index 00000000..3368930d --- /dev/null +++ b/renderer/settings/Provider.tsx @@ -0,0 +1,30 @@ +import { JSX } from 'solid-js'; +import { TransProvider } from '@jellybrick/solid-i18next'; + +import { Router } from '@solidjs/router'; + +import { LangResource } from '../../common/intl'; +import useConfig from '../hooks/useConfig'; + +export interface ProviderProps { + children: JSX.Element; +} + +const Provider = (props: ProviderProps) => { + const [config] = useConfig(); + + return ( + + + {props.children} + + + ); +}; + +export default Provider; diff --git a/renderer/settings/components/ColorPicker.tsx b/renderer/settings/components/ColorPicker.tsx index 16cc17f9..0ce1e4ad 100644 --- a/renderer/settings/components/ColorPicker.tsx +++ b/renderer/settings/components/ColorPicker.tsx @@ -82,10 +82,13 @@ const ColorPicker = (props: ColorPickerProps) => { class={'absolute left-[4px] right-[4px] h-[4px] bg-primary-500 rounded-full -z-1 origin-left'} />
diff --git a/renderer/settings/components/ListItem.tsx b/renderer/settings/components/ListItem.tsx index 457dc905..639f618f 100644 --- a/renderer/settings/components/ListItem.tsx +++ b/renderer/settings/components/ListItem.tsx @@ -18,10 +18,11 @@ const ListItem = (props: ListItemProps) => { ` relative w-full h-[36px] min-h-9 px-3 flex flex-row justify-start items-center gap-1 - rounded shadow-sm hover:bg-white/[7.5%] active:bg-white/5 - select-none + hover:shadow-sm hover:bg-black/[7.5%] active:shadow-sm active:bg-black/5 + dark:hover:bg-white/[7.5%] dark:active:bg-white/10 + select-none rounded `, - local.selected && 'bg-white/10', + local.selected && 'bg-black/5 dark:bg-white/5', leftProps.class, )} > diff --git a/renderer/settings/components/ListView.tsx b/renderer/settings/components/ListView.tsx index e2d19d09..9bd3d76b 100644 --- a/renderer/settings/components/ListView.tsx +++ b/renderer/settings/components/ListView.tsx @@ -33,7 +33,6 @@ const ListView = (props: ListViewProps) => { onMount(() => { const newTabHeight: number[] = []; - console.log(listParent); const offset = listParent?.getBoundingClientRect()?.y ?? 0; Array.from(listParent?.children ?? []) .forEach((item) => { @@ -58,8 +57,11 @@ const ListView = (props: ListViewProps) => { class={cx('relative flex flex-col justify-start items-start p-4 gap-1', leftProps.class)} >
{(item) => ( diff --git a/renderer/settings/containers/GameContainer.tsx b/renderer/settings/containers/GameContainer.tsx index a6cf6003..fe9d1a3a 100644 --- a/renderer/settings/containers/GameContainer.tsx +++ b/renderer/settings/containers/GameContainer.tsx @@ -1,8 +1,8 @@ -import { For, Match, Show, Switch, createEffect, createSignal } from 'solid-js'; +import { For, Show, createEffect, createSignal } from 'solid-js'; import { Trans, useTransContext } from '@jellybrick/solid-i18next'; import { Marquee } from '@suyongs/solid-utility'; -import GameListContainer from './GameListContainer'; +import { useNavigate } from '@solidjs/router'; import Card from '../../components/Card'; import useGameList from '../../hooks/useGameList'; @@ -20,8 +20,8 @@ interface ProcessData { const GameContainer = () => { const [showOnlyAvailable, setShowOnlyAvailable] = createSignal(true); const [processList, setProcessList] = createSignal([]); - const [pageState, setPageState] = createSignal('default'); + const navigate = useNavigate(); const [gameList, setGameList] = useGameList(); const playingGame = usePlayingGame(); const [t] = useTransContext(); @@ -60,113 +60,107 @@ const GameContainer = () => { setGameList(list, false); }; + const onGameListPage = () => { + navigate('/game-overlay/list'); + }; return ( - - -
-
- -
-
- -
- - {(game) => ( - -
-
- {game.name} -
- - {game.path} - -
-
- )} -
- - - - - -
- -
- setPageState('list')}> -
+
+
+ +
+
+ +
+ + {(game) => ( + +
- -
-
- + {game.name}
+ + {game.path} +
- - -
-
- - - - + )} + + + + + + +
+ +
+ +
+
+ +
+
+
+
+ + + +
+
+ + + + +
- - {(process) => ( - - onAddGame(process)}> - - - )} - > - - - - )} - - + )} > - - - { - showOnlyAvailable() ? - t('setting.game.show-all-programs-running-in-the-background') : - t('setting.game.show-only-programs-running-in-the-foreground') - } - -
- - - setPageState('default')} - /> - - + + + + )} +
+ +
); }; diff --git a/renderer/settings/containers/GameListContainer.tsx b/renderer/settings/containers/GameListContainer.tsx index 1d1df1a5..56c841d0 100644 --- a/renderer/settings/containers/GameListContainer.tsx +++ b/renderer/settings/containers/GameListContainer.tsx @@ -1,22 +1,21 @@ import { For, JSX, createEffect, createSignal } from 'solid-js'; import { Trans } from '@jellybrick/solid-i18next'; +import { useNavigate } from '@solidjs/router'; + import useGameList from '../../hooks/useGameList'; import GameCard from '../components/GameCard'; -export interface GameListContainerProps { - onBack?: () => void; -} - interface GameList { path: string; name: string; icon: string; } -const GameListContainer = (props: GameListContainerProps) => { +const GameListContainer = () => { const [availableGameList, setAvailableGameList] = createSignal([]); const [gameList, setGameList] = useGameList(); + const navigate = useNavigate(); const updateAvailableGameList = async () => { const gamePathList = Object.entries(gameList()) @@ -54,15 +53,18 @@ const GameListContainer = (props: GameListContainerProps) => { [file.path]: file.name, }); }; + const onGamePage = () => { + navigate('/game-overlay'); + }; return (
- props.onBack?.()}> + - + diff --git a/renderer/settings/containers/GeneralContainer.tsx b/renderer/settings/containers/GeneralContainer.tsx index 6055ab0b..29ae58ab 100644 --- a/renderer/settings/containers/GeneralContainer.tsx +++ b/renderer/settings/containers/GeneralContainer.tsx @@ -8,6 +8,7 @@ import Selector from '../../components/Select'; import useConfig from '../../hooks/useConfig'; import { getTranslation } from '../../../common/intl'; import Modal from '../../components/Modal'; +import Switch from '../../components/Switch'; const LanguageContainer = () => { const [t, { changeLanguage }] = useTransContext(); @@ -52,12 +53,7 @@ const LanguageContainer = () => {
- setConfig({ developer: checked })} - /> + setConfig({ developer: checked })} /> {
- - + + {
- - + + {
- - + +
diff --git a/renderer/settings/containers/InfoContainer.tsx b/renderer/settings/containers/InfoContainer.tsx index d22627a7..eb7c7d9c 100644 --- a/renderer/settings/containers/InfoContainer.tsx +++ b/renderer/settings/containers/InfoContainer.tsx @@ -39,7 +39,7 @@ const InfoContainer = () => { }; return ( -
+
@@ -52,13 +52,13 @@ const InfoContainer = () => {
Alspotron
-
+
https://github.com/organization/alspotron
- + { subCards={[
- +
{
-
+
{
-
+
@@ -113,19 +113,19 @@ const InfoContainer = () => {
- +
]} > - - + +
-
+
{packageJson.version}
@@ -139,13 +139,13 @@ const InfoContainer = () => {
Khinenw
-
+
,
- + onLink('https://github.com/Su-Yong')}> @@ -154,13 +154,13 @@ const InfoContainer = () => {
Su-Yong
-
+
- + onLink('https://github.com/JellyBrick')}> @@ -169,13 +169,13 @@ const InfoContainer = () => {
JellyBrick
-
+
- + onLink('https://github.com/smnis')}> @@ -184,13 +184,13 @@ const InfoContainer = () => {
SeongMin Park
-
+
- + onLink('https://github.com/alvin0319')}> @@ -199,13 +199,13 @@ const InfoContainer = () => {
alvin0319
-
+
- + onLink('https://github.com/SemteulGaram')}> @@ -214,13 +214,13 @@ const InfoContainer = () => {
STGR
-
+
- +
@@ -232,13 +232,13 @@ const InfoContainer = () => {
Hyeseo Lee
-
+
- + onLink('https://github.com/hwangseonu')}> @@ -247,13 +247,13 @@ const InfoContainer = () => {
mocha
-
+
- + onLink('https://github.com/ReturnToFirst')}> @@ -262,13 +262,13 @@ const InfoContainer = () => {
ReturnToFirst
-
+
- + onLink('https://github.com/sbaik2')}> @@ -277,13 +277,13 @@ const InfoContainer = () => {
Seungho Baik
-
+
- + onLink('https://github.com/Aden1126')}> @@ -292,13 +292,13 @@ const InfoContainer = () => {
Aden1126
-
+
- +
diff --git a/renderer/settings/containers/PositionContainer.tsx b/renderer/settings/containers/PositionContainer.tsx index d8f83d9c..179159c9 100644 --- a/renderer/settings/containers/PositionContainer.tsx +++ b/renderer/settings/containers/PositionContainer.tsx @@ -32,11 +32,10 @@ const PositionContainer = () => {
-
{ > -
+
@@ -156,7 +155,6 @@ const PositionContainer = () => { displays.map((display, index) => `${index + 1} - ${display.label}`) )} class={'select'} - popupClass={'p-1 bg-gray-800 rounded gap-1'} /> @@ -170,7 +168,6 @@ const PositionContainer = () => { onChange={(value) => setConfig({ windowPosition: { direction: value as 'column' | 'column-reverse' } })} options={['column', 'column-reverse']} class={'select'} - popupClass={'p-1 bg-gray-800 rounded gap-1'} /> @@ -184,18 +181,17 @@ const PositionContainer = () => { onChange={(value) => setConfig({ style: { nowPlaying: { visible: value === 'true' } } })} options={['true', 'false']} class={'select'} - popupClass={'p-1 bg-gray-800 rounded gap-1'} />
-
@@ -230,7 +226,7 @@ const PositionContainer = () => { value={config()?.windowPosition.bottom ?? undefined} onChange={(event) => setConfig({ windowPosition: { bottom: Number(event.target.value) } })} /> -
+
) }; diff --git a/renderer/settings/containers/ThemeContainer.tsx b/renderer/settings/containers/ThemeContainer.tsx index ccd880c9..cb3c3cf2 100644 --- a/renderer/settings/containers/ThemeContainer.tsx +++ b/renderer/settings/containers/ThemeContainer.tsx @@ -11,6 +11,7 @@ import LyricsTransition from '../../main/components/LyricsTransition'; import ColorPicker from '../components/ColorPicker'; import UserCSSEditor from '../components/UserCSSEditor'; import { cx } from '../../utils/classNames'; +import Switch from '../../components/Switch'; const ANIMATION_LIST = [ 'none', @@ -228,11 +229,9 @@ const ThemeContainer = () => {
- setConfig({ style: { animationAtOnce: checked } })} + setConfig({ style: { animationAtOnce: checked } })} />
, ]} @@ -241,7 +240,7 @@ const ThemeContainer = () => {
-
+
{getAnimationName(config()?.style?.animation ?? 'pretty')}
@@ -448,7 +447,7 @@ const ThemeContainer = () => {
-
+
diff --git a/renderer/settings/index.tsx b/renderer/settings/index.tsx index c9bad77f..ea2838dc 100644 --- a/renderer/settings/index.tsx +++ b/renderer/settings/index.tsx @@ -1,8 +1,13 @@ import { render } from 'solid-js/web'; import App from './App'; +import Provider from './Provider'; render( - App, + () => ( + + + + ), document && document.querySelector('#app')!, ); diff --git a/renderer/style.css b/renderer/style.css index a1808530..3329f4c0 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -8,15 +8,17 @@ } .btn-primary { - @apply min-w-[130px] px-3 py-2 rounded text-black - bg-primary-500 hover:bg-primary-600 active:opacity-50 + @apply min-w-[130px] px-3 py-2 rounded + text-black bg-primary-500 hover:bg-primary-600 active:opacity-50 + dark:text-white dark:bg-primary-500 dark:hover:bg-primary-400 outline-none select-none cursor-pointer focus:outline-none; } .btn-primary-disabled { - @apply min-w-[130px] px-3 py-2 rounded text-black - bg-primary-600 + @apply min-w-[130px] px-3 py-2 rounded opacity-50 + text-black/50 bg-primary-300 + dark:text-white/50 dark:bg-primary-700 outline-none select-none cursor-pointer focus:outline-none; } @@ -29,9 +31,10 @@ } .btn-text { - @apply min-w-[130px] px-3 py-2 rounded text-white - bg-white/[7.5%] hover:bg-white/10 active:bg-white/5 - outline-none select-none cursor-pointer + @apply min-w-[130px] px-3 py-2 rounded + text-black bg-white/60 hover:bg-white/40 active:bg-white/20 + dark:text-white dark:bg-white/5 dark:hover:bg-white/10 dark:active:bg-white/[2.5%] + shadow-sm outline-none select-none cursor-pointer focus:outline-none; } @@ -41,8 +44,9 @@ } .checkbox { - @apply appearance-none w-[20px] h-[20px] bg-gray-100/5 rounded-[4px] hover:bg-gray-100/10 focus:bg-black/10 - border-[1px] border-gray-100/20 + @apply appearance-none w-[20px] h-[20px] bg-gray-100/5 rounded-[4px] border-[1px] + dark:border-gray-100/20 dark:hover:bg-gray-100/10 dark:focus:bg-black/10 + border-black/20 hover:bg-black/10 focus:bg-white/10 } .checkbox:checked { @@ -50,9 +54,11 @@ } .input { - @apply bg-gray-100/5 text-white rounded-[4px] hover:bg-gray-100/10 focus:bg-black/10 - border-[1px] border-gray-100/[2%] border-b-gray-100/20 focus:border-b-2 focus:border-b-primary-500 focus:border-gray-100/10 - outline-none py-1 px-3 mb-[1px] focus:mb-[0px] + @apply bg-white/60 text-black hover:bg-white/50 focus:bg-white/40 + dark:bg-gray-600/60 dark:text-white dark:hover:bg-gray-600/50 dark:focus:bg-gray-600/40 + border-[1px] border-black/10 border-b-black/30 focus:border-b-2 focus:border-b-primary-500 + dark:border-white/5 dark:border-b-gray-400/50 + outline-none py-1 px-3 mb-[1px] focus:mb-[0px] rounded-[4px] } .input.color { @@ -64,9 +70,14 @@ } .select-container { - @apply w-fit flex justify-start items-center - bg-white/[7.5%] hover:bg-white/10 active:bg-white/5 - py-2 px-4 rounded-[4px]; + @apply bg-white/60 text-black hover:bg-white/50 active:bg-white/40 + dark:bg-gray-600/60 dark:text-white dark:hover:bg-gray-600/50 + w-fit flex justify-start items-center py-2 px-4 + rounded-[4px] border-[1px] border-black/10 border-b-black/30 + dark:border-white/5 dark:border-b-gray-400/50; + } + .select-container[data-active="true"] { + @apply border-b-primary-500 border-b-2; } .select { @apply outline-none bg-transparent w-fit; diff --git a/src/Application.ts b/src/Application.ts index af43d6ee..eb2b9db9 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -50,6 +50,8 @@ const micaOptions = { show: false, }; +const PlatformBrowserWindow = process.platform === 'win32' && IS_WINDOWS_11 ? MicaBrowserWindow : GlassBrowserWindow; + // Set application name for Windows 10+ notifications if (process.platform === 'win32') { app.setAppUserModelId(app.getName()); @@ -195,7 +197,8 @@ class Application { } else { this.initLyricsWindow(); } - } + }, + icon: nativeImage.createFromPath(getFile('./assets/empty.png')).resize({ width: 16, height: 16 }), }, { type: 'normal', @@ -214,6 +217,7 @@ class Application { }, { type: 'normal', + role: 'quit', label: getTranslation('tray.exit.label', config().language), click: () => { this.markQuit = true; @@ -228,6 +232,7 @@ class Application { type: 'separator', }, { + type: 'submenu', label: getTranslation('tray.devtools.label', config().language), submenu: [ { @@ -539,12 +544,13 @@ class Application { }); ipcMain.handle('get-game-list', () => gameList()); - ipcMain.on('window-minimize', () => BrowserWindow.getFocusedWindow()?.minimize()); - ipcMain.on('window-maximize', () => { - if (BrowserWindow.getFocusedWindow()?.isMaximized()) BrowserWindow.getFocusedWindow()?.unmaximize(); - else BrowserWindow.getFocusedWindow()?.maximize(); - }) - ipcMain.on('window-close', () => BrowserWindow.getFocusedWindow()?.close()); + ipcMain.handle('window-minimize', () => BrowserWindow.getFocusedWindow()?.minimize()); + ipcMain.handle('window-maximize', () => { + BrowserWindow.getFocusedWindow()?.isMaximized() ? + BrowserWindow.getFocusedWindow()?.unmaximize() : BrowserWindow.getFocusedWindow()?.maximize(); + }); + ipcMain.handle('window-is-maximized', () => BrowserWindow.getFocusedWindow()?.isMaximized()); + ipcMain.handle('window-close', () => BrowserWindow.getFocusedWindow()?.close()); ipcMain.handle('open-devtool', (_, target: string) => { if (target === 'main') { if (this.mainWindow && !this.mainWindow.isDestroyed()) { @@ -682,7 +688,7 @@ class Application { } initSettingsWindow() { - this.settingsWindow = new (process.platform === 'win32' && IS_WINDOWS_11 ? MicaBrowserWindow : GlassBrowserWindow)({ + this.settingsWindow = new PlatformBrowserWindow({ ...glassOptions, ...micaOptions, width: 800, @@ -694,14 +700,15 @@ class Application { title: getTranslation('title.setting', config().language), titleBarStyle: 'hiddenInset', frame: false, - vibrancy: 'dark', + transparent: true, + vibrancy: 'fullscreen-ui', autoHideMenuBar: true, icon: iconPath, }); if (this.settingsWindow instanceof MicaBrowserWindow) { - this.settingsWindow.setDarkTheme(); - this.settingsWindow.setMicaEffect(); + this.settingsWindow.setAutoTheme(); + this.settingsWindow.setMicaAcrylicEffect(); } this.settingsWindow.show(); @@ -721,7 +728,7 @@ class Application { } initLyricsWindow() { - this.lyricsWindow = new (process.platform === 'win32' && IS_WINDOWS_11 ? MicaBrowserWindow : GlassBrowserWindow)({ + this.lyricsWindow = new PlatformBrowserWindow({ ...glassOptions, ...micaOptions, width: 1000, @@ -733,14 +740,15 @@ class Application { title: getTranslation('title.lyrics', config().language), titleBarStyle: 'hiddenInset', frame: false, - vibrancy: 'dark', + transparent: true, + vibrancy: 'fullscreen-ui', autoHideMenuBar: true, icon: iconPath, }); if (this.lyricsWindow instanceof MicaBrowserWindow) { - this.lyricsWindow.setDarkTheme(); - this.lyricsWindow.setMicaEffect(); + this.lyricsWindow.setAutoTheme(); + this.lyricsWindow.setMicaAcrylicEffect(); } this.lyricsWindow.show();