Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add to SGF Library Directly From Game #2364

25 changes: 24 additions & 1 deletion src/views/Game/GameDock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import * as data from "data";
import * as preferences from "preferences";
import { MAX_DOCK_DELAY } from "SettingsCommon";
import { useUser } from "hooks";
import { api1, post, del } from "requests";
import { api1, post, del, get } from "requests";
import { Dock } from "Dock";
import { Link } from "react-router-dom";
import { toast } from "toast";
import { _, pgettext } from "translate";
import { openACLModal } from "ACLModal";
import { openGameLinkModal } from "./GameLinkModal";
import { openGameLogModal } from "./GameLogModal";
import { openGameLibraryModal } from "./GameLibraryModal";
import { sfx } from "sfx";
import { alert } from "swal_config";
import { challengeFromBoardPosition } from "ChallengeModal/ForkModal";
Expand Down Expand Up @@ -311,6 +312,18 @@ export function GameDock({
? engine.playerToMove()
: engine.playerNotToMove();

const showLibraryModal = () => {
// Pull user data prior to opening modal preventing component from rendering prior to obtaining needed data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to open the modal and show a Loading indicator, you can probably just drop in <Loading /> while you wait for the data, that way people know their action has been registered and they just need to be patient while we wait for the internet.

const user = data.get("user");
const promise = get("library/%%", user.id);
promise
.then(async (library) => {
const userLibrary = await library.collections;
openGameLibraryModal(user.id, userLibrary, game_id);
})
.catch(errorAlerter);
};

return (
<Dock>
{(tournament_id || null) && (
Expand Down Expand Up @@ -499,13 +512,23 @@ export function GameDock({
<i className="fa fa-download"></i> {_("Download SGF")}
</a>
)}

{/* Add To Library functionality */}

{sgf_download_enabled && sgf_with_ai_review_url && (
<Tooltip tooltipRequired={tooltipRequired} title={_("SGF with AI Review")}>
<a href={sgf_with_ai_review_url} target="_blank">
<i className="fa fa-download"></i> {_("SGF with AI Review")}
</a>
</Tooltip>
)}
{
<Tooltip tooltipRequired={tooltipRequired} title={_("Save to Library")}>
<a onClick={showLibraryModal}>
<i className="fa fa-book"></i> {_("Save to Library")}
</a>
</Tooltip>
}
{sgf_download_enabled && sgf_with_comments_url && (
<Tooltip tooltipRequired={tooltipRequired} title={_("SGF with comments")}>
<a href={sgf_with_comments_url} target="_blank">
Expand Down
51 changes: 51 additions & 0 deletions src/views/Game/GameLibraryModal.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (C) Online-Go.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

.GameLibraryModal.Modal {
width: 100%;
max-width: 40rem;
padding: 1rem;
display: flex;
flex-direction: row;
align-items: center;

.file-name-inputfield{
margin: 1rem;
}
}
.collection-list {
display:flex;
width: 100%;
flex-direction: column;
align-content: stretch;
align-items: center;
}

.collection-row {
width: 100%
display:flex;
justify-content: space-evenly;
align-items: center;
}

.cell {
max-width: 20rem;
padding-left: 0.5em;
display: table-cell;
text-align: left;
position: relative;
}
176 changes: 176 additions & 0 deletions src/views/Game/GameLibraryModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright (C) Online-Go.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import * as React from "react";
import { openModal, Modal } from "Modal";
import { api1, post } from "requests";
import { errorAlerter } from "misc";
import { _ } from "translate";

interface Events {}

interface GameLibraryModalProperties {
userID: number;
userLibrary: any;
gameID: number;
}

export class GameLibraryModal extends Modal<Events, GameLibraryModalProperties, any> {
constructor(props) {
super(props);
this.state = {
gameName: "",
collectionName: "",
collections: [],
gameAdded: false,
};
}

componentDidMount(): void {
const userLibrary: any = JSON.stringify(this.props.userLibrary);
const userlibraryJSON = JSON.parse(userLibrary);
this.setState({ collections: userlibraryJSON });
}

setGameName = (ev) => {
this.setState({ gameName: ev.target.value });
};

setCollectionName = (ev) => {
this.setState({ collectionName: ev.target.value });
};

resetTextFields = () => {
this.setState({ gameName: "" });
this.setState({ collectionName: "" });
};

async addToLibrary(collection) {
const url = api1(`games/${this.props.gameID}/sgf`);
await fetch(url, {
method: "GET",
// Specify the content-type of the request otherwise the "games/%%/sgf" endpoint will respond with a 415 error code
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.blob())
.then((data) => {
// Pull sgfName from value implemented by user setting a default value if empty
let gameName = this.state.gameName;
if (this.state.gameName === "") {
gameName = `My Game #${this.props.gameID}`;
}
const gameFile = new File([data as BlobPart], `${gameName}.sgf`, {
type: "application/x-go-sgf",
lastModified: new Date().getTime(),
});
// Create post request adding SGF File to the given collection using collection's id
post("me/games/sgf/%%", collection[0], gameFile).catch(errorAlerter);
this.resetTextFields();
this.close(); // Closes modal after creating post request
this.setState({ gameAdded: true });
})
.catch(errorAlerter);
}

async createCollection() {
if (this.state.collectionName === "") {
await this.setState({ collectionName: "My_Collection" });
}
// Create a new library within the user's libraries
post("library/%%/collections", this.props.userID, {
name: this.state.collectionName,
})
.then((res) => {
// Pull the newly created collection's id to be used to identify which collection we add our SGF File too
const collection_id = [res.collection_id];
this.addToLibrary(collection_id).catch(errorAlerter);
})
.catch(errorAlerter);
}

render() {
return (
<div className="Modal GameLibraryModal">
<div className="collection-list">
<h1>Libraries</h1>

{/* If user has collections in their library map and render them. Otherwise; allow user to create collection */}
{this.state.collections.length > 0 ? (
<>
<span>
<i className="fa fa-file" />
<input
className="file-name-inputfield"
type="text"
value={this.state.gameName}
onChange={this.setGameName}
placeholder={_("Insert SGF File Name")}
/>
</span>

{this.state.collections.map((data) => (
<div className="collection-row" key={data.id}>
<span className="cell">
<h1>{data[1]}</h1>
</span>
<span className="cell">
<button onClick={() => this.addToLibrary(data)}>add</button>
</span>
</div>
))}
</>
) : (
<div className="collection-row">
<span className="cell">
<i className="fa fa-file" />
<input
className="file-name-inputfield"
type="text"
value={this.state.gameName}
onChange={this.setGameName}
placeholder={_("Insert SGF File Name")}
/>
</span>
<span className="cell">
<i className="fa fa-folder" />
<input
type="text"
value={this.state.collectionName}
onChange={this.setCollectionName}
placeholder={_("Insert Collection Name")}
/>
</span>
<span className="cell">
<button onClick={() => this.createCollection()}>
Create Collection
</button>
</span>
</div>
)}
</div>
</div>
);
}
}

export function openGameLibraryModal(userID: number, userLibrary: any, gameID: number): void {
openModal(
<GameLibraryModal userID={userID} userLibrary={userLibrary} gameID={gameID} fastDismiss />,
);
}
Loading